@modusensus/dsh-mneme 0.7.9 → 0.7.11
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.en.md +368 -0
- package/README.md +298 -167
- package/{dsh-mneme/cordis.patch.yml → cordis.patch.yml} +1 -0
- package/{dsh-mneme/lib → lib}/api.js +58 -284
- package/lib/client.js +2086 -0
- package/{dsh-mneme/src → lib}/config.js +6 -100
- package/{dsh-mneme/lib → lib}/dream/decisions.js +94 -143
- package/{dsh-mneme/src → lib}/dream/sleep.js +12 -152
- package/{dsh-mneme/lib → lib}/dream.js +22 -168
- package/{dsh-mneme/src → lib}/hot-memory.js +0 -7
- package/{dsh-mneme/src → lib}/index.js +6 -34
- package/{dsh-mneme/lib → lib}/inject.js +11 -53
- package/{dsh-mneme/src → lib}/mirror.js +1 -12
- package/{dsh-mneme/lib → lib}/quality-filter.js +1 -3
- package/{dsh-mneme/lib → lib}/reranker.js +1 -4
- package/{dsh-mneme/lib → lib}/service.js +20 -410
- package/{dsh-mneme/lib → lib}/settings.js +0 -70
- package/{dsh-mneme/lib → lib}/store.js +35 -450
- package/{dsh-mneme/lib → lib}/summarize.js +10 -10
- package/lib/tools.js +274 -0
- package/package.json +41 -19
- package/{dsh-mneme/scripts → scripts}/e2e-dsh.js +3 -3
- package/{dsh-mneme/scripts → scripts}/sync-lib.js +5 -10
- package/{dsh-mneme/src → src}/api.js +58 -284
- package/{dsh-mneme/lib → src}/config.js +6 -100
- package/{dsh-mneme/src → src}/dream/decisions.js +94 -143
- package/{dsh-mneme/lib → src}/dream/sleep.js +12 -152
- package/{dsh-mneme/src → src}/dream.js +22 -168
- package/{dsh-mneme/lib → src}/hot-memory.js +0 -7
- package/{dsh-mneme/lib → src}/index.js +6 -34
- package/{dsh-mneme/src → src}/inject.js +11 -53
- package/{dsh-mneme/lib → src}/mirror.js +1 -12
- package/{dsh-mneme/src → src}/quality-filter.js +1 -3
- package/{dsh-mneme/src → src}/reranker.js +1 -4
- package/{dsh-mneme/src → src}/service.js +20 -410
- package/{dsh-mneme/src → src}/settings.js +0 -70
- package/{dsh-mneme/src → src}/store.js +35 -450
- package/{dsh-mneme/src → src}/summarize.js +10 -10
- package/src/tools.js +274 -0
- package/{dsh-mneme/test → test}/api.test.js +1 -156
- package/{dsh-mneme/test → test}/client.test.js +22 -207
- package/{dsh-mneme/test → test}/config.test.js +0 -21
- package/{dsh-mneme/test → test}/dream.test.js +1 -160
- package/{dsh-mneme/test → test}/graph-api.test.js +0 -34
- package/{dsh-mneme/test → test}/hot-memory.test.js +0 -29
- package/test/inject.test.js +120 -0
- package/{dsh-mneme/test → test}/llm-audit.test.js +4 -4
- package/{dsh-mneme/test → test}/reasoning-effort.test.js +0 -27
- package/{dsh-mneme/test → test}/recall-layer.test.js +5 -26
- package/{dsh-mneme/test → test}/reranker.test.js +0 -43
- package/{dsh-mneme/test → test}/service-search.test.js +0 -25
- package/{dsh-mneme/test → test}/service.test.js +0 -106
- package/{dsh-mneme/test → test}/settings.test.js +0 -43
- package/{dsh-mneme/test → test}/sleep.test.js +4 -171
- package/{dsh-mneme/test → test}/store.test.js +0 -76
- package/{dsh-mneme/test → test}/summarize.test.js +16 -15
- package/test/tools.test.js +265 -0
- package/.github/workflows/publish.yml +0 -58
- package/.github/workflows/test.yml +0 -32
- package/CHANGELOG.md +0 -91
- package/CONTRIBUTING.md +0 -291
- package/SECURITY.md +0 -718
- package/docs/devlog/2026-08-14-dsh-mneme-dev-log.md +0 -247
- package/docs/devlog/2026-08-15-dsh-mneme-audit-stress-dev-log.md +0 -145
- package/docs/devlog/2026-08-15-dsh-mneme-pipeline-dev-log.md +0 -56
- package/docs/devlog/2026-08-15-dsh-mneme-reflection-dev-log.md +0 -77
- package/docs/devlog/2026-08-15-dsh-mneme-review-fixes-dev-log.md +0 -64
- package/docs/devlog/2026-08-15-dsh-mneme-semantic-dev-log.md +0 -90
- package/dsh-mneme/CHANGELOG.md +0 -406
- package/dsh-mneme/LICENSE +0 -21
- package/dsh-mneme/README.md +0 -498
- package/dsh-mneme/docs/AGENT_MEMORY_RESEARCH.md +0 -183
- package/dsh-mneme/docs/ENTITIES.md +0 -245
- package/dsh-mneme/docs/LOCAL_MODEL.md +0 -141
- package/dsh-mneme/docs/MIGRATION.md +0 -127
- package/dsh-mneme/docs/SEMANTIC.md +0 -256
- package/dsh-mneme/docs/SLEEP.md +0 -163
- package/dsh-mneme/lib/client.js +0 -2050
- package/dsh-mneme/lib/dream/tag-extractor.js +0 -156
- package/dsh-mneme/lib/heat.js +0 -136
- package/dsh-mneme/lib/parser/tag.js +0 -59
- package/dsh-mneme/lib/parser/wiki-link.js +0 -38
- package/dsh-mneme/lib/search/tag-boost.js +0 -61
- package/dsh-mneme/lib/tools.js +0 -458
- package/dsh-mneme/package-lock.json +0 -1936
- package/dsh-mneme/package.json +0 -83
- package/dsh-mneme/scripts/check-sync.js +0 -42
- package/dsh-mneme/src/client.js +0 -2050
- package/dsh-mneme/src/dream/tag-extractor.js +0 -156
- package/dsh-mneme/src/heat.js +0 -136
- package/dsh-mneme/src/parser/tag.js +0 -59
- package/dsh-mneme/src/parser/wiki-link.js +0 -38
- package/dsh-mneme/src/search/tag-boost.js +0 -61
- package/dsh-mneme/src/tools.js +0 -458
- package/dsh-mneme/test/boundary-v0625.test.js +0 -82
- package/dsh-mneme/test/directory.test.js +0 -134
- package/dsh-mneme/test/heat.test.js +0 -148
- package/dsh-mneme/test/inject.test.js +0 -206
- package/dsh-mneme/test/layered-types-stats.test.js +0 -144
- package/dsh-mneme/test/lib-smoke.test.js +0 -109
- package/dsh-mneme/test/normalize-decisions.test.js +0 -120
- package/dsh-mneme/test/provenance.test.js +0 -103
- package/dsh-mneme/test/recall-runs.test.js +0 -93
- package/dsh-mneme/test/sleep-heat.test.js +0 -112
- package/dsh-mneme/test/tag-boost.test.js +0 -125
- package/dsh-mneme/test/tag.test.js +0 -426
- package/dsh-mneme/test/tools.test.js +0 -674
- package/dsh-mneme/test/updated-at-semantics.test.js +0 -113
- package/dsh-mneme/test/wiki-link.test.js +0 -332
- package//346/250/252/345/271/205.png +0 -0
- /package/{dsh-mneme/lib → lib}/commands.js +0 -0
- /package/{dsh-mneme/lib → lib}/dream/clustering.js +0 -0
- /package/{dsh-mneme/lib → lib}/embedding.js +0 -0
- /package/{dsh-mneme/lib → lib}/entities/extractor.js +0 -0
- /package/{dsh-mneme/lib → lib}/local-embedder.js +0 -0
- /package/{dsh-mneme/lib → lib}/search/adaptive.js +0 -0
- /package/{dsh-mneme/lib → lib}/search/bm25.js +0 -0
- /package/{dsh-mneme/lib → lib}/vector-index.js +0 -0
- /package/{dsh-mneme/scripts → scripts}/benchmark-embed.js +0 -0
- /package/{dsh-mneme/scripts → scripts}/benchmark-recall.js +0 -0
- /package/{dsh-mneme/scripts → scripts}/benchmark-rerank.js +0 -0
- /package/{dsh-mneme/scripts → scripts}/stress-dsh.js +0 -0
- /package/{dsh-mneme/src → src}/commands.js +0 -0
- /package/{dsh-mneme/src → src}/dream/clustering.js +0 -0
- /package/{dsh-mneme/src → src}/embedding.js +0 -0
- /package/{dsh-mneme/src → src}/entities/extractor.js +0 -0
- /package/{dsh-mneme/src → src}/local-embedder.js +0 -0
- /package/{dsh-mneme/src → src}/search/adaptive.js +0 -0
- /package/{dsh-mneme/src → src}/search/bm25.js +0 -0
- /package/{dsh-mneme/src → src}/vector-index.js +0 -0
- /package/{dsh-mneme/test → test}/audit.test.js +0 -0
- /package/{dsh-mneme/test → test}/benchmark.test.js +0 -0
- /package/{dsh-mneme/test → test}/clustering.test.js +0 -0
- /package/{dsh-mneme/test → test}/commands.test.js +0 -0
- /package/{dsh-mneme/test → test}/conflict-freeze.test.js +0 -0
- /package/{dsh-mneme/test → test}/entities.test.js +0 -0
- /package/{dsh-mneme/test → test}/epistemic.test.js +0 -0
- /package/{dsh-mneme/test → test}/fnew-0112.test.js +0 -0
- /package/{dsh-mneme/test → test}/fnew-03.test.js +0 -0
- /package/{dsh-mneme/test → test}/helpers/dream-mock.js +0 -0
- /package/{dsh-mneme/test → test}/local-embedder.test.js +0 -0
- /package/{dsh-mneme/test → test}/mirror-dirty.test.js +0 -0
- /package/{dsh-mneme/test → test}/mirror-edit-digest.test.js +0 -0
- /package/{dsh-mneme/test → test}/mirror-generation.test.js +0 -0
- /package/{dsh-mneme/test → test}/mirror.test.js +0 -0
- /package/{dsh-mneme/test → test}/peer-blockers.test.js +0 -0
- /package/{dsh-mneme/test → test}/policy-epoch.test.js +0 -0
- /package/{dsh-mneme/test → test}/quality-filter.test.js +0 -0
- /package/{dsh-mneme/test → test}/recall-evals.test.js +0 -0
- /package/{dsh-mneme/test → test}/receipt-chain.test.js +0 -0
- /package/{dsh-mneme/test → test}/reflection.test.js +0 -0
- /package/{dsh-mneme/test → test}/search-fusion.test.js +0 -0
- /package/{dsh-mneme/test → test}/semantic.test.js +0 -0
- /package/{dsh-mneme/test → test}/stress.test.js +0 -0
- /package/{dsh-mneme/test → test}/vector-index.test.js +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { BlockAssembler, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
2
2
|
|
|
3
3
|
const SUMMARY_PROMPT = `你是记忆库提炼助手。根据下面的会话内容,提炼 2-3 条值得跨会话记住的记忆。
|
|
4
|
-
只输出 JSON 数组,每项形如 {"type":"preference|project|decision|history
|
|
4
|
+
只输出 JSON 数组,每项形如 {"type":"preference|project|decision|history","title":"简短标题","content":"一句话内容","importance":1-5}。
|
|
5
5
|
不要输出任何其他文字。`;
|
|
6
6
|
|
|
7
7
|
/** Extract a JSON array from LLM output that may contain prose around it. */
|
|
@@ -17,7 +17,7 @@ export function parseSummaryJson(raw) {
|
|
|
17
17
|
return [];
|
|
18
18
|
}
|
|
19
19
|
if (!Array.isArray(arr)) return [];
|
|
20
|
-
const VALID = new Set(["preference", "project", "decision", "history"
|
|
20
|
+
const VALID = new Set(["preference", "project", "decision", "history"]);
|
|
21
21
|
return arr.filter(
|
|
22
22
|
(item) =>
|
|
23
23
|
item &&
|
|
@@ -76,11 +76,15 @@ function toProtocolChunk(chunk) {
|
|
|
76
76
|
// events must not leak into the memory store. Events without a data payload
|
|
77
77
|
// (minimal test doubles) pass the kind check and are handled by the content
|
|
78
78
|
// check below.
|
|
79
|
+
// DSH ≥0.1.2-rc removed the Session.events property; events are only reachable
|
|
80
|
+
// via snapshotEvents(). Older DSH builds still expose .events, so fall back.
|
|
81
|
+
function getSessionEvents(session) {
|
|
82
|
+
return session?.snapshotEvents?.() ?? session?.events ?? [];
|
|
83
|
+
}
|
|
84
|
+
|
|
79
85
|
function collectMessages(session) {
|
|
80
86
|
const messages = [];
|
|
81
|
-
|
|
82
|
-
const events = session.snapshotEvents?.() ?? session.events ?? [];
|
|
83
|
-
for (const event of events) {
|
|
87
|
+
for (const event of getSessionEvents(session)) {
|
|
84
88
|
const kind = event.data?.source?.kind;
|
|
85
89
|
if (event.type !== "user/message") continue;
|
|
86
90
|
if (kind !== undefined && kind !== "user") continue;
|
|
@@ -184,11 +188,7 @@ export function createSummarizer(ctx, service, config) {
|
|
|
184
188
|
.join("");
|
|
185
189
|
const entries = parseSummaryJson(text || assembledText);
|
|
186
190
|
for (const entry of entries) {
|
|
187
|
-
|
|
188
|
-
// session.id is always available here — it rides both the human-readable
|
|
189
|
-
// source label and the structured session_id column (v0.5.x memory
|
|
190
|
-
// provenance, the raw material for v0.6.0 reasoning-path / drift analysis).
|
|
191
|
-
service.saveWithDedupe({ ...entry, source: `session:${session.id}`, session_id: session.id });
|
|
191
|
+
service.saveWithDedupe({ ...entry, source: `session:${session.id}` });
|
|
192
192
|
}
|
|
193
193
|
} finally {
|
|
194
194
|
if (audit) {
|
package/src/tools.js
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
2
|
+
|
|
3
|
+
const TEXT_OUTPUT = (text) => [{ type: "text", text }];
|
|
4
|
+
|
|
5
|
+
// Wire shape emitted by service.toApiList: shared by memory_search and
|
|
6
|
+
// memory_list so their output schemas always declare every key the runtime
|
|
7
|
+
// value carries (additionalProperties: false would reject undeclared keys).
|
|
8
|
+
const MEMORY_ITEM_SCHEMA = {
|
|
9
|
+
type: "object",
|
|
10
|
+
additionalProperties: false,
|
|
11
|
+
properties: {
|
|
12
|
+
id: { type: "string", required: true },
|
|
13
|
+
type: { type: "string", required: true },
|
|
14
|
+
title: { type: "string", required: true },
|
|
15
|
+
content: { type: "string", required: true },
|
|
16
|
+
tags: { type: "array", items: { type: "string" } },
|
|
17
|
+
importance: { type: "integer", required: true },
|
|
18
|
+
source: { type: "string" },
|
|
19
|
+
created_at: { type: "string", required: true },
|
|
20
|
+
updated_at: { type: "string", required: true }
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function createTools(ctx, service, config, embedder) {
|
|
25
|
+
const tools = [
|
|
26
|
+
defineTool({
|
|
27
|
+
name: "memory_save",
|
|
28
|
+
description:
|
|
29
|
+
"Persist one memory entry for future sessions (user preferences, project state, decisions). " +
|
|
30
|
+
"Call this when the user states a durable preference, a project decision is made, or a lesson is learned. " +
|
|
31
|
+
"Merges into an existing entry of the same type when the title matches.",
|
|
32
|
+
parameters: {
|
|
33
|
+
type: { type: "string", required: true, enum: ["preference", "project", "decision", "history"], description: "preference=user profile; project=project knowledge/state; decision=key decision; history=conversation summary" },
|
|
34
|
+
title: { type: "string", required: true, description: "Short unique title" },
|
|
35
|
+
content: { type: "string", required: true, description: "Memory body" },
|
|
36
|
+
tags: { type: "array", items: { type: "string" }, description: "Optional tags" },
|
|
37
|
+
importance: { type: "integer", description: "1-5; >= threshold auto-injects into future sessions" },
|
|
38
|
+
source: { type: "string", description: "Optional provenance" }
|
|
39
|
+
},
|
|
40
|
+
output: {
|
|
41
|
+
schema: {
|
|
42
|
+
type: "object",
|
|
43
|
+
additionalProperties: false,
|
|
44
|
+
properties: {
|
|
45
|
+
action: { type: "string", required: true, enum: ["created", "merged"] },
|
|
46
|
+
id: { type: "string", required: true }
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
render: (_args, value) => TEXT_OUTPUT(`memory ${value.action}: ${value.id}`)
|
|
50
|
+
},
|
|
51
|
+
async execute(args) {
|
|
52
|
+
const { action, memory } = service.saveWithDedupe({
|
|
53
|
+
type: args.type,
|
|
54
|
+
title: args.title,
|
|
55
|
+
content: args.content,
|
|
56
|
+
tags: args.tags ?? [],
|
|
57
|
+
importance: args.importance ?? 3,
|
|
58
|
+
source: args.source ?? "tool"
|
|
59
|
+
});
|
|
60
|
+
return { action, id: memory.id };
|
|
61
|
+
}
|
|
62
|
+
}),
|
|
63
|
+
|
|
64
|
+
defineTool({
|
|
65
|
+
name: "memory_search",
|
|
66
|
+
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.",
|
|
67
|
+
parameters: {
|
|
68
|
+
query: { type: "string", required: true, description: "Search text; substring match over title/content/tags" },
|
|
69
|
+
limit: { type: "integer", description: "Max results (default 20)" },
|
|
70
|
+
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" },
|
|
71
|
+
semantic: { type: "boolean", description: "Shorthand: enable semantic (vector) recall (same as mode=vector when true)" },
|
|
72
|
+
rerank: { type: "boolean", description: "Run cross-encoder rerank over candidates when a local reranker is configured (default true)" }
|
|
73
|
+
},
|
|
74
|
+
output: {
|
|
75
|
+
schema: {
|
|
76
|
+
type: "object",
|
|
77
|
+
additionalProperties: false,
|
|
78
|
+
properties: {
|
|
79
|
+
items: {
|
|
80
|
+
type: "array", required: true,
|
|
81
|
+
items: MEMORY_ITEM_SCHEMA
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
render: (_args, value) => TEXT_OUTPUT(`Found ${value.items.length} memory entr${value.items.length === 1 ? "y" : "ies"}.`)
|
|
86
|
+
},
|
|
87
|
+
async execute(args) {
|
|
88
|
+
const limit = args.limit ?? 20;
|
|
89
|
+
const mode = args.semantic === true && !args.mode ? "vector" : args.mode ?? "auto";
|
|
90
|
+
const rows = await service.searchMemories(args.query, {
|
|
91
|
+
mode,
|
|
92
|
+
topK: limit,
|
|
93
|
+
useRerank: args.rerank !== false
|
|
94
|
+
});
|
|
95
|
+
return { items: service.toApiList(rows) };
|
|
96
|
+
}
|
|
97
|
+
}),
|
|
98
|
+
|
|
99
|
+
defineTool({
|
|
100
|
+
name: "memory_list",
|
|
101
|
+
description: "List memory entries by type, high-importance first, then newest, paginated. Set include_archived=true to also list archived (hidden) entries so they can be located and restored.",
|
|
102
|
+
parameters: {
|
|
103
|
+
type: { type: "string", enum: ["preference", "project", "decision", "history"], description: "Filter by type; omit for all" },
|
|
104
|
+
limit: { type: "integer", description: "Page size (default 50)" },
|
|
105
|
+
offset: { type: "integer", description: "Page offset (default 0)" },
|
|
106
|
+
include_archived: { type: "boolean", description: "Include archived (hidden) entries so they can be found and restored (default false)" }
|
|
107
|
+
},
|
|
108
|
+
output: {
|
|
109
|
+
schema: {
|
|
110
|
+
type: "object",
|
|
111
|
+
additionalProperties: false,
|
|
112
|
+
properties: {
|
|
113
|
+
items: {
|
|
114
|
+
type: "array", required: true,
|
|
115
|
+
items: MEMORY_ITEM_SCHEMA
|
|
116
|
+
},
|
|
117
|
+
total: { type: "integer", required: true }
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
render: (_args, value) => TEXT_OUTPUT(`${value.items.length} memory entries (of ${value.total}).`)
|
|
121
|
+
},
|
|
122
|
+
async execute(args) {
|
|
123
|
+
const includeArchived = args.include_archived === true;
|
|
124
|
+
const rows = service.toApiList(service.list({
|
|
125
|
+
type: args.type,
|
|
126
|
+
limit: args.limit ?? 50,
|
|
127
|
+
offset: args.offset ?? 0,
|
|
128
|
+
includeArchived
|
|
129
|
+
}));
|
|
130
|
+
return { items: rows, total: service.count(args.type, { includeArchived }) };
|
|
131
|
+
}
|
|
132
|
+
}),
|
|
133
|
+
|
|
134
|
+
defineTool({
|
|
135
|
+
name: "memory_update",
|
|
136
|
+
description: "Modify an existing memory entry (title, content, type, tags, importance).",
|
|
137
|
+
parameters: {
|
|
138
|
+
id: { type: "string", required: true, description: "Memory id" },
|
|
139
|
+
title: { type: "string" },
|
|
140
|
+
content: { type: "string" },
|
|
141
|
+
type: { type: "string", enum: ["preference", "project", "decision", "history"] },
|
|
142
|
+
tags: { type: "array", items: { type: "string" } },
|
|
143
|
+
importance: { type: "integer", description: "1-5" },
|
|
144
|
+
reason: { type: "string", description: "Optional context for the correction (what the user actually said/wanted), recorded for reflection" }
|
|
145
|
+
},
|
|
146
|
+
output: {
|
|
147
|
+
schema: {
|
|
148
|
+
type: "object",
|
|
149
|
+
additionalProperties: false,
|
|
150
|
+
properties: {
|
|
151
|
+
memory: {
|
|
152
|
+
type: "object",
|
|
153
|
+
additionalProperties: false,
|
|
154
|
+
properties: {
|
|
155
|
+
id: { type: "string", required: true },
|
|
156
|
+
title: { type: "string", required: true },
|
|
157
|
+
content: { type: "string", required: true }
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
render: (_args, value) => TEXT_OUTPUT(`Updated memory ${value.memory.id}: ${value.memory.title}`)
|
|
163
|
+
},
|
|
164
|
+
async execute(args) {
|
|
165
|
+
const memory = service.update(args.id, {
|
|
166
|
+
title: args.title,
|
|
167
|
+
content: args.content,
|
|
168
|
+
type: args.type,
|
|
169
|
+
tags: args.tags,
|
|
170
|
+
importance: args.importance
|
|
171
|
+
}, { query: args.reason });
|
|
172
|
+
return { memory: { id: memory.id, title: memory.title, content: memory.content } };
|
|
173
|
+
}
|
|
174
|
+
}),
|
|
175
|
+
|
|
176
|
+
defineTool({
|
|
177
|
+
name: "memory_delete",
|
|
178
|
+
description: "Permanently delete a memory entry.",
|
|
179
|
+
parameters: {
|
|
180
|
+
id: { type: "string", required: true }
|
|
181
|
+
},
|
|
182
|
+
output: {
|
|
183
|
+
schema: {
|
|
184
|
+
type: "object",
|
|
185
|
+
additionalProperties: false,
|
|
186
|
+
properties: { deleted: { type: "boolean", required: true } }
|
|
187
|
+
},
|
|
188
|
+
render: (_args, value) => TEXT_OUTPUT(value.deleted ? "Memory deleted." : "Memory not found.")
|
|
189
|
+
},
|
|
190
|
+
async execute(args) {
|
|
191
|
+
const existed = service.getById(args.id) !== undefined;
|
|
192
|
+
if (existed) service.remove(args.id);
|
|
193
|
+
return { deleted: existed };
|
|
194
|
+
}
|
|
195
|
+
}),
|
|
196
|
+
|
|
197
|
+
defineTool({
|
|
198
|
+
name: "memory_forget",
|
|
199
|
+
description:
|
|
200
|
+
"Stop a memory from being auto-injected and from appearing in searches and lists without deleting it. " +
|
|
201
|
+
"The entry stays in storage; pass forgotten: false to restore it.",
|
|
202
|
+
parameters: {
|
|
203
|
+
id: { type: "string", required: true },
|
|
204
|
+
forgotten: { type: "boolean", description: "Suppress (true, default) or restore (false) the entry's visibility" }
|
|
205
|
+
},
|
|
206
|
+
output: {
|
|
207
|
+
schema: {
|
|
208
|
+
type: "object",
|
|
209
|
+
additionalProperties: false,
|
|
210
|
+
properties: {
|
|
211
|
+
memory: {
|
|
212
|
+
type: "object",
|
|
213
|
+
additionalProperties: false,
|
|
214
|
+
properties: {
|
|
215
|
+
id: { type: "string", required: true },
|
|
216
|
+
forgotten: { type: "boolean", required: true }
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
render: (_args, value) => TEXT_OUTPUT(`Memory ${value.memory.id} injection ${value.memory.forgotten ? "suppressed" : "restored"}.`)
|
|
222
|
+
},
|
|
223
|
+
async execute(args) {
|
|
224
|
+
if (service.getById(args.id) === undefined) {
|
|
225
|
+
throw new Error("memory not found");
|
|
226
|
+
}
|
|
227
|
+
const memory = service.setForget(args.id, args.forgotten ?? true);
|
|
228
|
+
return { memory: { id: memory.id, forgotten: memory.forgotten } };
|
|
229
|
+
}
|
|
230
|
+
}),
|
|
231
|
+
|
|
232
|
+
defineTool({
|
|
233
|
+
name: "memory_archive",
|
|
234
|
+
description:
|
|
235
|
+
"Archive a memory (hide it from active lists, search, injection and dream consolidation) or restore it. " +
|
|
236
|
+
"Archived entries stay in storage and are recoverable: pass archived=false to restore, and use memory_list with " +
|
|
237
|
+
"include_archived=true to find archived entries.",
|
|
238
|
+
parameters: {
|
|
239
|
+
id: { type: "string", required: true, description: "Memory id" },
|
|
240
|
+
archived: { type: "boolean", description: "Archive (true, default) or restore (false) the entry" }
|
|
241
|
+
},
|
|
242
|
+
output: {
|
|
243
|
+
schema: {
|
|
244
|
+
type: "object",
|
|
245
|
+
additionalProperties: false,
|
|
246
|
+
properties: {
|
|
247
|
+
memory: {
|
|
248
|
+
type: "object",
|
|
249
|
+
additionalProperties: false,
|
|
250
|
+
properties: {
|
|
251
|
+
id: { type: "string", required: true },
|
|
252
|
+
archived: { type: "boolean", required: true }
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
render: (_args, value) => TEXT_OUTPUT(`Memory ${value.memory.id} ${value.memory.archived ? "archived" : "restored"}.`)
|
|
258
|
+
},
|
|
259
|
+
async execute(args) {
|
|
260
|
+
if (service.getById(args.id) === undefined) {
|
|
261
|
+
throw new Error("memory not found");
|
|
262
|
+
}
|
|
263
|
+
const memory = service.setArchived(args.id, args.archived ?? true);
|
|
264
|
+
return { memory: { id: memory.id, archived: memory.archived } };
|
|
265
|
+
}
|
|
266
|
+
})
|
|
267
|
+
];
|
|
268
|
+
|
|
269
|
+
for (const tool of tools) {
|
|
270
|
+
ctx.tools.register(tool);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return tools;
|
|
274
|
+
}
|
|
@@ -29,10 +29,8 @@ function req(path, method = "GET", body = null) {
|
|
|
29
29
|
|
|
30
30
|
function setup(embedder, apiToken = "") {
|
|
31
31
|
const store = createStore(":memory:");
|
|
32
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
32
33
|
const settings = createSettings(store.db);
|
|
33
|
-
// settings is threaded into the service so the /config endpoint reports the
|
|
34
|
-
// effective settings-over-config tag toggles (issue #31).
|
|
35
|
-
const service = createService({ store, mirror: null, config: {}, settings });
|
|
36
34
|
const commands = {
|
|
37
35
|
add: (def) => settings.addCommand(def),
|
|
38
36
|
remove: (id) => settings.removeCommand(id),
|
|
@@ -469,156 +467,3 @@ test("Bug10: vector-reindex with an embed-only OpenAI-compatible embedder return
|
|
|
469
467
|
assert.equal(vectorIndex.dimension(), 3, "dimension written to vector_meta");
|
|
470
468
|
assert.equal(vectorIndex.getEmbedding(service.all()[0].id).length, 3, "embedding persisted");
|
|
471
469
|
});
|
|
472
|
-
|
|
473
|
-
// --- memory tags (v0.6.2) ---------------------------------------------------
|
|
474
|
-
|
|
475
|
-
test("GET /api/dsh-mneme/memory/tags returns live tags and the manual gate", async () => {
|
|
476
|
-
const { routes, service } = setup();
|
|
477
|
-
const mem = service.saveWithDedupe({ type: "preference", title: "A", content: "x" }).memory;
|
|
478
|
-
service.setMemoryTags(mem.id, ["bash", "考研"]);
|
|
479
|
-
const route = routes.find((r) => r.path === "/api/dsh-mneme/memory/tags");
|
|
480
|
-
const res = new FakeRes();
|
|
481
|
-
await route.handler(req(`/api/dsh-mneme/memory/tags?id=${mem.id}`), res);
|
|
482
|
-
assert.equal(res.statusCode, 200);
|
|
483
|
-
const data = JSON.parse(res.body);
|
|
484
|
-
assert.deepEqual(data.tags, ["bash", "考研"], "authoritative entity_attrs tags");
|
|
485
|
-
assert.equal(data.manualTagEnabled, true, "gate defaults on");
|
|
486
|
-
});
|
|
487
|
-
|
|
488
|
-
test("POST /api/dsh-mneme/memory/tags overwrites the tag set", async () => {
|
|
489
|
-
const { routes, service } = setup();
|
|
490
|
-
const mem = service.saveWithDedupe({ type: "preference", title: "A", content: "x" }).memory;
|
|
491
|
-
const route = routes.find((r) => r.path === "/api/dsh-mneme/memory/tags");
|
|
492
|
-
const res = new FakeRes();
|
|
493
|
-
await route.handler(req("/api/dsh-mneme/memory/tags", "POST", { id: mem.id, tags: ["linux"] }), res);
|
|
494
|
-
assert.equal(res.statusCode, 200);
|
|
495
|
-
const data = JSON.parse(res.body);
|
|
496
|
-
assert.equal(data.ok, true);
|
|
497
|
-
assert.deepEqual(data.tags, ["linux"]);
|
|
498
|
-
assert.deepEqual(service.getMemoryTags(mem.id), ["linux"], "tag set persisted");
|
|
499
|
-
});
|
|
500
|
-
|
|
501
|
-
test("POST /api/dsh-mneme/memory/tags 409s when manualTagEnabled is off", async () => {
|
|
502
|
-
const store = createStore(":memory:");
|
|
503
|
-
const service = createService({ store, mirror: null, config: { manualTagEnabled: false } });
|
|
504
|
-
const settings = createSettings(store.db);
|
|
505
|
-
const routes = [];
|
|
506
|
-
const ctx = { webServer: { register(route) { routes.push(route); return () => {}; } } };
|
|
507
|
-
createApi(ctx, service, settings, { add() {}, remove() {}, list() { return []; } });
|
|
508
|
-
const mem = service.saveWithDedupe({ type: "preference", title: "A", content: "x" }).memory;
|
|
509
|
-
const route = routes.find((r) => r.path === "/api/dsh-mneme/memory/tags");
|
|
510
|
-
const res = new FakeRes();
|
|
511
|
-
await route.handler(req("/api/dsh-mneme/memory/tags", "POST", { id: mem.id, tags: ["x"] }), res);
|
|
512
|
-
assert.equal(res.statusCode, 409, "manual tag write rejected");
|
|
513
|
-
assert.match(JSON.parse(res.body).error, /manualTagEnabled/);
|
|
514
|
-
const get = new FakeRes();
|
|
515
|
-
await route.handler(req(`/api/dsh-mneme/memory/tags?id=${mem.id}`), get);
|
|
516
|
-
assert.equal(JSON.parse(get.body).manualTagEnabled, false, "GET reports the gate off");
|
|
517
|
-
});
|
|
518
|
-
|
|
519
|
-
// v0.6.3 directory view endpoint: /api/dsh-mneme/directory returns the
|
|
520
|
-
// tag-grouped directory (groups + untagged) as JSON.
|
|
521
|
-
test("GET /api/dsh-mneme/directory groups tagged memories by tag", async () => {
|
|
522
|
-
const { routes, service } = setup();
|
|
523
|
-
const a = service.saveWithDedupe({ type: "preference", title: "Linux", content: "内核" }).memory;
|
|
524
|
-
const b = service.saveWithDedupe({ type: "preference", title: "考研", content: "公共管理" }).memory;
|
|
525
|
-
service.setMemoryTags(a.id, ["linux"]);
|
|
526
|
-
service.setMemoryTags(b.id, ["考研"]);
|
|
527
|
-
const route = routes.find((r) => r.path === "/api/dsh-mneme/directory");
|
|
528
|
-
assert.ok(route, "directory route must be registered");
|
|
529
|
-
const res = new FakeRes();
|
|
530
|
-
await route.handler(req("/api/dsh-mneme/directory"), res);
|
|
531
|
-
assert.equal(res.statusCode, 200);
|
|
532
|
-
const data = JSON.parse(res.body);
|
|
533
|
-
assert.deepEqual(Object.keys(data).sort(), ["groups", "untagged"]);
|
|
534
|
-
assert.equal(data.groups.length, 2);
|
|
535
|
-
const linux = data.groups.find((g) => g.tag === "linux");
|
|
536
|
-
assert.deepEqual(linux.memories.map((m) => m.id), [a.id]);
|
|
537
|
-
assert.equal(linux.memories[0].title, "Linux");
|
|
538
|
-
});
|
|
539
|
-
|
|
540
|
-
test("GET /api/dsh-mneme/directory reports untagged memories", async () => {
|
|
541
|
-
const { routes, service } = setup();
|
|
542
|
-
const tagged = service.saveWithDedupe({ type: "preference", title: "Tagged", content: "x" }).memory;
|
|
543
|
-
const bare = service.saveWithDedupe({ type: "project", title: "Bare", content: "y" }).memory;
|
|
544
|
-
service.setMemoryTags(tagged.id, ["linux"]);
|
|
545
|
-
const route = routes.find((r) => r.path === "/api/dsh-mneme/directory");
|
|
546
|
-
const res = new FakeRes();
|
|
547
|
-
await route.handler(req("/api/dsh-mneme/directory"), res);
|
|
548
|
-
assert.equal(res.statusCode, 200);
|
|
549
|
-
const data = JSON.parse(res.body);
|
|
550
|
-
assert.deepEqual(data.untagged.map((m) => m.id), [bare.id]);
|
|
551
|
-
});
|
|
552
|
-
|
|
553
|
-
test("GET/PUT /api/dsh-mneme/config round-trips autoTag switches", async () => {
|
|
554
|
-
const { routes } = setup();
|
|
555
|
-
const route = routes.find((r) => r.path === "/api/dsh-mneme/config");
|
|
556
|
-
const res1 = new FakeRes();
|
|
557
|
-
await route.handler(req("/api/dsh-mneme/config"), res1);
|
|
558
|
-
assert.equal(res1.statusCode, 200);
|
|
559
|
-
// unset → effective config falls back to plugin-config defaults:
|
|
560
|
-
// autoTagEnabled=false, manualTagEnabled=true (issue #31 default unification),
|
|
561
|
-
// showSidebarTrigger=true (issue #38 default)
|
|
562
|
-
assert.deepEqual(JSON.parse(res1.body).config, { autoTagEnabled: false, manualTagEnabled: true, showSidebarTrigger: true });
|
|
563
|
-
const res2 = new FakeRes();
|
|
564
|
-
await route.handler(req("/api/dsh-mneme/config", "PUT", { autoTagEnabled: true, manualTagEnabled: false }), res2);
|
|
565
|
-
assert.equal(res2.statusCode, 200);
|
|
566
|
-
assert.deepEqual(JSON.parse(res2.body).config, { autoTagEnabled: true, manualTagEnabled: false, showSidebarTrigger: true });
|
|
567
|
-
});
|
|
568
|
-
|
|
569
|
-
test("GET/PUT /api/dsh-mneme/config round-trips the sidebar trigger toggle (issue #38)", async () => {
|
|
570
|
-
const { routes } = setup();
|
|
571
|
-
const route = routes.find((r) => r.path === "/api/dsh-mneme/config");
|
|
572
|
-
const res = new FakeRes();
|
|
573
|
-
await route.handler(req("/api/dsh-mneme/config", "PUT", { showSidebarTrigger: false }), res);
|
|
574
|
-
assert.equal(res.statusCode, 200);
|
|
575
|
-
// Toggling the UI pref must not disturb the tag toggles (settings-over-config).
|
|
576
|
-
assert.deepEqual(JSON.parse(res.body).config, { autoTagEnabled: false, manualTagEnabled: true, showSidebarTrigger: false });
|
|
577
|
-
const res2 = new FakeRes();
|
|
578
|
-
await route.handler(req("/api/dsh-mneme/config"), res2);
|
|
579
|
-
assert.deepEqual(JSON.parse(res2.body).config, { autoTagEnabled: false, manualTagEnabled: true, showSidebarTrigger: false });
|
|
580
|
-
});
|
|
581
|
-
|
|
582
|
-
test("DELETE /api/dsh-mneme/memories removes by id and reports misses", async () => {
|
|
583
|
-
const { routes, service } = setup();
|
|
584
|
-
service.saveWithDedupe({ type: "preference", title: "语言", content: "中文" });
|
|
585
|
-
const target = service.list().find((m) => m.title === "语言");
|
|
586
|
-
const id = target.id;
|
|
587
|
-
const route = routes.find((r) => r.path === "/api/dsh-mneme/memories");
|
|
588
|
-
const res = new FakeRes();
|
|
589
|
-
await route.handler(req("/api/dsh-mneme/memories", "DELETE", { id }), res);
|
|
590
|
-
assert.equal(res.statusCode, 200);
|
|
591
|
-
assert.equal(JSON.parse(res.body).deleted, true);
|
|
592
|
-
const res2 = new FakeRes();
|
|
593
|
-
await route.handler(req("/api/dsh-mneme/memories", "DELETE", { id }), res2);
|
|
594
|
-
assert.equal(JSON.parse(res2.body).deleted, false);
|
|
595
|
-
});
|
|
596
|
-
|
|
597
|
-
test("DELETE /api/dsh-mneme/memories rejects id+query together", async () => {
|
|
598
|
-
const { routes } = setup();
|
|
599
|
-
const route = routes.find((r) => r.path === "/api/dsh-mneme/memories");
|
|
600
|
-
const res = new FakeRes();
|
|
601
|
-
await route.handler(req("/api/dsh-mneme/memories", "DELETE", { id: "x", query: "y" }), res);
|
|
602
|
-
assert.equal(res.statusCode, 400);
|
|
603
|
-
});
|
|
604
|
-
|
|
605
|
-
test("PUT /api/dsh-mneme/config supports partial update", async () => {
|
|
606
|
-
const { routes } = setup();
|
|
607
|
-
const route = routes.find((r) => r.path === "/api/dsh-mneme/config");
|
|
608
|
-
const res = new FakeRes();
|
|
609
|
-
await route.handler(req("/api/dsh-mneme/config", "PUT", { autoTagEnabled: true }), res);
|
|
610
|
-
assert.equal(res.statusCode, 200);
|
|
611
|
-
// Partial PUT only stores autoTagEnabled; manualTagEnabled stays unset and
|
|
612
|
-
// falls back to the plugin-config default (true) — a lone autoTag toggle must
|
|
613
|
-
// never silently disable manual tagging (issue #31).
|
|
614
|
-
assert.deepEqual(JSON.parse(res.body).config, { autoTagEnabled: true, manualTagEnabled: true, showSidebarTrigger: true });
|
|
615
|
-
});
|
|
616
|
-
|
|
617
|
-
test("PUT /api/dsh-mneme/config rejects non-boolean showSidebarTrigger", async () => {
|
|
618
|
-
const { routes } = setup();
|
|
619
|
-
const route = routes.find((r) => r.path === "/api/dsh-mneme/config");
|
|
620
|
-
const res = new FakeRes();
|
|
621
|
-
await route.handler(req("/api/dsh-mneme/config", "PUT", { showSidebarTrigger: "no" }), res);
|
|
622
|
-
assert.equal(res.statusCode, 400);
|
|
623
|
-
assert.match(JSON.parse(res.body).error, /showSidebarTrigger/);
|
|
624
|
-
});
|