@modusensus/dsh-mneme 0.7.4 → 0.7.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/.github/workflows/test.yml +32 -0
- package/CHANGELOG.md +89 -0
- package/CONTRIBUTING.md +245 -0
- package/README.md +159 -421
- package/SECURITY.md +674 -0
- package/docs/devlog/2026-08-14-dsh-mneme-dev-log.md +247 -0
- package/docs/devlog/2026-08-15-dsh-mneme-audit-stress-dev-log.md +145 -0
- package/docs/devlog/2026-08-15-dsh-mneme-pipeline-dev-log.md +56 -0
- package/docs/devlog/2026-08-15-dsh-mneme-reflection-dev-log.md +77 -0
- package/docs/devlog/2026-08-15-dsh-mneme-review-fixes-dev-log.md +64 -0
- package/docs/devlog/2026-08-15-dsh-mneme-semantic-dev-log.md +90 -0
- package/dsh-mneme/CHANGELOG.md +358 -0
- package/dsh-mneme/LICENSE +21 -0
- package/dsh-mneme/README.md +493 -0
- package/dsh-mneme/docs/AGENT_MEMORY_RESEARCH.md +183 -0
- package/dsh-mneme/docs/ENTITIES.md +245 -0
- package/dsh-mneme/docs/LOCAL_MODEL.md +141 -0
- package/dsh-mneme/docs/MIGRATION.md +127 -0
- package/dsh-mneme/docs/SEMANTIC.md +256 -0
- package/dsh-mneme/docs/SLEEP.md +163 -0
- package/{src → dsh-mneme/lib}/api.js +15 -0
- package/{lib → dsh-mneme/lib}/client.js +187 -2
- package/{lib → dsh-mneme/lib}/mirror.js +3 -1
- package/{lib → dsh-mneme/lib}/quality-filter.js +3 -1
- package/{src → dsh-mneme/lib}/service.js +9 -5
- package/{lib → dsh-mneme/lib}/store.js +35 -1
- package/{lib → dsh-mneme/lib}/summarize.js +2 -2
- package/{src → dsh-mneme/lib}/tools.js +3 -3
- package/dsh-mneme/package-lock.json +1936 -0
- package/dsh-mneme/package.json +83 -0
- package/{lib → dsh-mneme/src}/api.js +15 -0
- package/{src → dsh-mneme/src}/mirror.js +3 -1
- package/{src → dsh-mneme/src}/quality-filter.js +3 -1
- package/{lib → dsh-mneme/src}/service.js +9 -5
- package/{src → dsh-mneme/src}/store.js +35 -1
- package/{src → dsh-mneme/src}/summarize.js +2 -2
- package/{lib → dsh-mneme/src}/tools.js +3 -3
- package/dsh-mneme/test/layered-types-stats.test.js +144 -0
- package/package.json +18 -43
- package//346/250/252/345/271/205.png +0 -0
- /package/{cordis.patch.yml → dsh-mneme/cordis.patch.yml} +0 -0
- /package/{lib → dsh-mneme/lib}/commands.js +0 -0
- /package/{lib → dsh-mneme/lib}/config.js +0 -0
- /package/{lib → dsh-mneme/lib}/dream/clustering.js +0 -0
- /package/{lib → dsh-mneme/lib}/dream/decisions.js +0 -0
- /package/{lib → dsh-mneme/lib}/dream/sleep.js +0 -0
- /package/{lib → dsh-mneme/lib}/dream/tag-extractor.js +0 -0
- /package/{lib → dsh-mneme/lib}/dream.js +0 -0
- /package/{lib → dsh-mneme/lib}/embedding.js +0 -0
- /package/{lib → dsh-mneme/lib}/entities/extractor.js +0 -0
- /package/{lib → dsh-mneme/lib}/heat.js +0 -0
- /package/{lib → dsh-mneme/lib}/hot-memory.js +0 -0
- /package/{lib → dsh-mneme/lib}/index.js +0 -0
- /package/{lib → dsh-mneme/lib}/inject.js +0 -0
- /package/{lib → dsh-mneme/lib}/local-embedder.js +0 -0
- /package/{lib → dsh-mneme/lib}/parser/tag.js +0 -0
- /package/{lib → dsh-mneme/lib}/parser/wiki-link.js +0 -0
- /package/{lib → dsh-mneme/lib}/reranker.js +0 -0
- /package/{lib → dsh-mneme/lib}/search/adaptive.js +0 -0
- /package/{lib → dsh-mneme/lib}/search/bm25.js +0 -0
- /package/{lib → dsh-mneme/lib}/search/tag-boost.js +0 -0
- /package/{lib → dsh-mneme/lib}/settings.js +0 -0
- /package/{lib → dsh-mneme/lib}/vector-index.js +0 -0
- /package/{scripts → dsh-mneme/scripts}/benchmark-embed.js +0 -0
- /package/{scripts → dsh-mneme/scripts}/benchmark-recall.js +0 -0
- /package/{scripts → dsh-mneme/scripts}/benchmark-rerank.js +0 -0
- /package/{scripts → dsh-mneme/scripts}/e2e-dsh.js +0 -0
- /package/{scripts → dsh-mneme/scripts}/stress-dsh.js +0 -0
- /package/{scripts → dsh-mneme/scripts}/sync-lib.js +0 -0
- /package/{src → dsh-mneme/src}/commands.js +0 -0
- /package/{src → dsh-mneme/src}/config.js +0 -0
- /package/{src → dsh-mneme/src}/dream/clustering.js +0 -0
- /package/{src → dsh-mneme/src}/dream/decisions.js +0 -0
- /package/{src → dsh-mneme/src}/dream/sleep.js +0 -0
- /package/{src → dsh-mneme/src}/dream/tag-extractor.js +0 -0
- /package/{src → dsh-mneme/src}/dream.js +0 -0
- /package/{src → dsh-mneme/src}/embedding.js +0 -0
- /package/{src → dsh-mneme/src}/entities/extractor.js +0 -0
- /package/{src → dsh-mneme/src}/heat.js +0 -0
- /package/{src → dsh-mneme/src}/hot-memory.js +0 -0
- /package/{src → dsh-mneme/src}/index.js +0 -0
- /package/{src → dsh-mneme/src}/inject.js +0 -0
- /package/{src → dsh-mneme/src}/local-embedder.js +0 -0
- /package/{src → dsh-mneme/src}/parser/tag.js +0 -0
- /package/{src → dsh-mneme/src}/parser/wiki-link.js +0 -0
- /package/{src → dsh-mneme/src}/reranker.js +0 -0
- /package/{src → dsh-mneme/src}/search/adaptive.js +0 -0
- /package/{src → dsh-mneme/src}/search/bm25.js +0 -0
- /package/{src → dsh-mneme/src}/search/tag-boost.js +0 -0
- /package/{src → dsh-mneme/src}/settings.js +0 -0
- /package/{src → dsh-mneme/src}/vector-index.js +0 -0
- /package/{test → dsh-mneme/test}/api.test.js +0 -0
- /package/{test → dsh-mneme/test}/audit.test.js +0 -0
- /package/{test → dsh-mneme/test}/benchmark.test.js +0 -0
- /package/{test → dsh-mneme/test}/boundary-v0625.test.js +0 -0
- /package/{test → dsh-mneme/test}/client.test.js +0 -0
- /package/{test → dsh-mneme/test}/clustering.test.js +0 -0
- /package/{test → dsh-mneme/test}/commands.test.js +0 -0
- /package/{test → dsh-mneme/test}/config.test.js +0 -0
- /package/{test → dsh-mneme/test}/conflict-freeze.test.js +0 -0
- /package/{test → dsh-mneme/test}/directory.test.js +0 -0
- /package/{test → dsh-mneme/test}/dream.test.js +0 -0
- /package/{test → dsh-mneme/test}/entities.test.js +0 -0
- /package/{test → dsh-mneme/test}/epistemic.test.js +0 -0
- /package/{test → dsh-mneme/test}/fnew-0112.test.js +0 -0
- /package/{test → dsh-mneme/test}/fnew-03.test.js +0 -0
- /package/{test → dsh-mneme/test}/graph-api.test.js +0 -0
- /package/{test → dsh-mneme/test}/heat.test.js +0 -0
- /package/{test → dsh-mneme/test}/helpers/dream-mock.js +0 -0
- /package/{test → dsh-mneme/test}/hot-memory.test.js +0 -0
- /package/{test → dsh-mneme/test}/inject.test.js +0 -0
- /package/{test → dsh-mneme/test}/llm-audit.test.js +0 -0
- /package/{test → dsh-mneme/test}/local-embedder.test.js +0 -0
- /package/{test → dsh-mneme/test}/mirror-dirty.test.js +0 -0
- /package/{test → dsh-mneme/test}/mirror-edit-digest.test.js +0 -0
- /package/{test → dsh-mneme/test}/mirror-generation.test.js +0 -0
- /package/{test → dsh-mneme/test}/mirror.test.js +0 -0
- /package/{test → dsh-mneme/test}/normalize-decisions.test.js +0 -0
- /package/{test → dsh-mneme/test}/peer-blockers.test.js +0 -0
- /package/{test → dsh-mneme/test}/policy-epoch.test.js +0 -0
- /package/{test → dsh-mneme/test}/provenance.test.js +0 -0
- /package/{test → dsh-mneme/test}/quality-filter.test.js +0 -0
- /package/{test → dsh-mneme/test}/reasoning-effort.test.js +0 -0
- /package/{test → dsh-mneme/test}/recall-evals.test.js +0 -0
- /package/{test → dsh-mneme/test}/recall-layer.test.js +0 -0
- /package/{test → dsh-mneme/test}/recall-runs.test.js +0 -0
- /package/{test → dsh-mneme/test}/receipt-chain.test.js +0 -0
- /package/{test → dsh-mneme/test}/reflection.test.js +0 -0
- /package/{test → dsh-mneme/test}/reranker.test.js +0 -0
- /package/{test → dsh-mneme/test}/search-fusion.test.js +0 -0
- /package/{test → dsh-mneme/test}/semantic.test.js +0 -0
- /package/{test → dsh-mneme/test}/service-search.test.js +0 -0
- /package/{test → dsh-mneme/test}/service.test.js +0 -0
- /package/{test → dsh-mneme/test}/settings.test.js +0 -0
- /package/{test → dsh-mneme/test}/sleep-heat.test.js +0 -0
- /package/{test → dsh-mneme/test}/sleep.test.js +0 -0
- /package/{test → dsh-mneme/test}/store.test.js +0 -0
- /package/{test → dsh-mneme/test}/stress.test.js +0 -0
- /package/{test → dsh-mneme/test}/summarize.test.js +0 -0
- /package/{test → dsh-mneme/test}/tag-boost.test.js +0 -0
- /package/{test → dsh-mneme/test}/tag.test.js +0 -0
- /package/{test → dsh-mneme/test}/tools.test.js +0 -0
- /package/{test → dsh-mneme/test}/updated-at-semantics.test.js +0 -0
- /package/{test → dsh-mneme/test}/vector-index.test.js +0 -0
- /package/{test → dsh-mneme/test}/wiki-link.test.js +0 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@modusensus/dsh-mneme",
|
|
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.7.5",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/modusensus/dsh-mneme.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/modusensus/dsh-mneme#readme",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/modusensus/dsh-mneme/issues"
|
|
13
|
+
},
|
|
14
|
+
"type": "module",
|
|
15
|
+
"main": "lib/index.js",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"default": "./lib/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./client": {
|
|
21
|
+
"default": "./lib/client.js"
|
|
22
|
+
},
|
|
23
|
+
"./package.json": "./package.json"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"lib",
|
|
27
|
+
"src",
|
|
28
|
+
"scripts",
|
|
29
|
+
"test",
|
|
30
|
+
"cordis.patch.yml"
|
|
31
|
+
],
|
|
32
|
+
"dsh": {
|
|
33
|
+
"client": {
|
|
34
|
+
"inject": [
|
|
35
|
+
"slots",
|
|
36
|
+
"locale",
|
|
37
|
+
"layout",
|
|
38
|
+
"connection"
|
|
39
|
+
],
|
|
40
|
+
"platform": "web"
|
|
41
|
+
},
|
|
42
|
+
"bundle": {
|
|
43
|
+
"patch": "./cordis.patch.yml"
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
48
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
|
|
49
|
+
"@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
|
|
50
|
+
"@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.6",
|
|
51
|
+
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
|
|
52
|
+
"@deepseek-ai/schemastery": "^3.18.1"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
56
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
|
|
57
|
+
"@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
|
|
58
|
+
"@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.6",
|
|
59
|
+
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
|
|
60
|
+
"@deepseek-ai/schemastery": "^3.18.1",
|
|
61
|
+
"c8": "^12.0.0"
|
|
62
|
+
},
|
|
63
|
+
"scripts": {
|
|
64
|
+
"sync": "node scripts/sync-lib.js",
|
|
65
|
+
"prepack": "npm run sync",
|
|
66
|
+
"test": "node --test test/*.test.js",
|
|
67
|
+
"test:coverage": "c8 node --test test/*.test.js",
|
|
68
|
+
"e2e": "node scripts/e2e-dsh.js",
|
|
69
|
+
"stress": "node scripts/stress-dsh.js"
|
|
70
|
+
},
|
|
71
|
+
"dependencies": {
|
|
72
|
+
"@huggingface/transformers": "^4.2.0"
|
|
73
|
+
},
|
|
74
|
+
"overrides": {
|
|
75
|
+
"adm-zip": "0.6.0"
|
|
76
|
+
},
|
|
77
|
+
"c8": {
|
|
78
|
+
"reporter": [
|
|
79
|
+
"text",
|
|
80
|
+
"lcov"
|
|
81
|
+
]
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -96,6 +96,21 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
96
96
|
}
|
|
97
97
|
});
|
|
98
98
|
|
|
99
|
+
register({
|
|
100
|
+
kind: "exact",
|
|
101
|
+
path: "/api/dsh-mneme/stats",
|
|
102
|
+
handler(req, res) {
|
|
103
|
+
try {
|
|
104
|
+
const url = new URL(req.url, "http://localhost");
|
|
105
|
+
const raw = url.searchParams.get("days");
|
|
106
|
+
const days = Math.min(30, Math.max(1, parseInt(raw ?? "7", 10) || 7));
|
|
107
|
+
sendJson(res, 200, service.stats({ days }));
|
|
108
|
+
} catch {
|
|
109
|
+
sendJson(res, 500, { error: "internal" });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
99
114
|
register({
|
|
100
115
|
kind: "exact",
|
|
101
116
|
path: "/api/dsh-mneme/search",
|
|
@@ -34,7 +34,9 @@ const TYPE_LABELS = {
|
|
|
34
34
|
decision: ["decision", "决策", "决定"],
|
|
35
35
|
history: ["history", "历史", "事件"],
|
|
36
36
|
summary: ["summary", "总结", "摘要", "总览"],
|
|
37
|
-
pattern: ["pattern", "模式", "规律"]
|
|
37
|
+
pattern: ["pattern", "模式", "规律"],
|
|
38
|
+
user: ["user", "用户"],
|
|
39
|
+
fact: ["fact", "事实", "原子事实"]
|
|
38
40
|
};
|
|
39
41
|
|
|
40
42
|
/** Normalized bigram-overlap similarity in [0,1]; 0 for tiny/empty inputs. */
|
|
@@ -7,7 +7,7 @@ import { parseWikiLinks } from "./parser/wiki-link.js";
|
|
|
7
7
|
import { extractQueryTags, applyTagBoost } from "./search/tag-boost.js";
|
|
8
8
|
import { computeHeat } from "./heat.js";
|
|
9
9
|
|
|
10
|
-
const INJECT_TYPES = new Set(["preference", "project", "decision", "summary"]);
|
|
10
|
+
const INJECT_TYPES = new Set(["preference", "project", "decision", "summary", "user", "fact"]);
|
|
11
11
|
|
|
12
12
|
// Epistemic trust weights (v0.4.5): when config.trustEpistemicWeighting is on,
|
|
13
13
|
// each recall candidate's existing score is multiplied by the weight of its
|
|
@@ -1047,12 +1047,15 @@ export function createService({ store, mirror, config, onWrite, logger, settings
|
|
|
1047
1047
|
// (quality_score null) count as 100 (weight 1), so legacy stores keep their
|
|
1048
1048
|
// exact summary>preference>importance ordering.
|
|
1049
1049
|
const qualityWeight = (m) => (m.quality_score != null ? m.quality_score / 100 : 1);
|
|
1050
|
+
// summary/user/preference are injected regardless of importance (context
|
|
1051
|
+
// layers); every other INJECT_TYPES type (e.g. fact) only when
|
|
1052
|
+
// importance >= threshold — the frontend "常注入" badge maps to this split.
|
|
1050
1053
|
const items = store.list({ limit: 200, includeForgotten: false })
|
|
1051
1054
|
.filter((m) => !m.archived && INJECT_TYPES.has(m.type) && !m.forgotten &&
|
|
1052
|
-
(m.type === "summary" || m.type === "preference" || m.importance >= threshold))
|
|
1055
|
+
(m.type === "summary" || m.type === "preference" || m.type === "user" || m.importance >= threshold))
|
|
1053
1056
|
.sort((a, b) => {
|
|
1054
|
-
const pa = a.type === "summary" ? 0 : a.type === "preference" ? 1 : 2;
|
|
1055
|
-
const pb = b.type === "summary" ? 0 : b.type === "preference" ? 1 : 2;
|
|
1057
|
+
const pa = a.type === "summary" ? 0 : (a.type === "preference" || a.type === "user") ? 1 : 2;
|
|
1058
|
+
const pb = b.type === "summary" ? 0 : (b.type === "preference" || b.type === "user") ? 1 : 2;
|
|
1056
1059
|
return pa - pb || (b.importance * qualityWeight(b)) - (a.importance * qualityWeight(a));
|
|
1057
1060
|
});
|
|
1058
1061
|
let candidates = items;
|
|
@@ -1068,7 +1071,7 @@ export function createService({ store, mirror, config, onWrite, logger, settings
|
|
|
1068
1071
|
const hits = vectorIndex.search(queryVector, { limit: maxItems * 2, threshold: 0 });
|
|
1069
1072
|
for (const m of hits) {
|
|
1070
1073
|
if (m && !m.archived && INJECT_TYPES.has(m.type) && !m.forgotten &&
|
|
1071
|
-
(m.type === "summary" || m.type === "preference" || m.importance >= threshold)) {
|
|
1074
|
+
(m.type === "summary" || m.type === "preference" || m.type === "user" || m.importance >= threshold)) {
|
|
1072
1075
|
semanticItems.push(m);
|
|
1073
1076
|
}
|
|
1074
1077
|
}
|
|
@@ -1606,6 +1609,7 @@ export function createService({ store, mirror, config, onWrite, logger, settings
|
|
|
1606
1609
|
list: (o) => store.list(o),
|
|
1607
1610
|
all: () => store.all(),
|
|
1608
1611
|
count: (type, opts) => store.count(type, opts),
|
|
1612
|
+
stats: (opts) => store.stats(opts),
|
|
1609
1613
|
getById: (id) => store.getById(id),
|
|
1610
1614
|
remove: (id) => {
|
|
1611
1615
|
store.remove(id);
|
|
@@ -242,7 +242,7 @@ CREATE TABLE IF NOT EXISTS mirror_state (
|
|
|
242
242
|
);
|
|
243
243
|
`;
|
|
244
244
|
|
|
245
|
-
const TYPES = new Set(["preference", "project", "decision", "history", "summary", "pattern"]);
|
|
245
|
+
const TYPES = new Set(["preference", "project", "decision", "history", "summary", "pattern", "user", "fact"]);
|
|
246
246
|
|
|
247
247
|
// Epistemic status: what kind of evidence a memory rests on. Defaults to
|
|
248
248
|
// 'subjective' so legacy rows (and rows without any signal) stay compatible.
|
|
@@ -659,6 +659,39 @@ export function createStore(path) {
|
|
|
659
659
|
return db.prepare(`SELECT count(*) AS c FROM memories ${where}`).get(...params).c;
|
|
660
660
|
}
|
|
661
661
|
|
|
662
|
+
/**
|
|
663
|
+
* Aggregated stats for the Web panel layered overview: per-type distribution
|
|
664
|
+
* plus a recent daily creation trend. Counts live (non-forgotten /
|
|
665
|
+
* non-archived / not session-disposed) memories only, matching `count`.
|
|
666
|
+
* created_at is ISO TEXT, so date strings compare lexicographically.
|
|
667
|
+
*/
|
|
668
|
+
function stats({ days = 7 } = {}) {
|
|
669
|
+
const LIVE = "forgotten = 0 AND archived = 0 AND session_disposed_at IS NULL";
|
|
670
|
+
// Clamp + integer-coerce so fractional strings (e.g. ?days=7.5) can't
|
|
671
|
+
// produce ragged trend windows (kimi-k2.7-code 复验 S2).
|
|
672
|
+
days = Math.min(30, Math.max(1, Math.floor(Number(days) || 7)));
|
|
673
|
+
const now = Date.now(); // single clock read so the window doesn't span a day boundary
|
|
674
|
+
const byType = db.prepare(
|
|
675
|
+
`SELECT type, count(*) AS c FROM memories WHERE ${LIVE} GROUP BY type`
|
|
676
|
+
).all();
|
|
677
|
+
const byTypeCounts = {};
|
|
678
|
+
for (const row of byType) byTypeCounts[row.type] = row.c;
|
|
679
|
+
const since = new Date(now - (days - 1) * 86400000).toISOString().slice(0, 10);
|
|
680
|
+
const trend = db.prepare(
|
|
681
|
+
`SELECT substr(created_at, 1, 10) AS d, count(*) AS c FROM memories
|
|
682
|
+
WHERE ${LIVE} AND created_at >= ? GROUP BY d ORDER BY d`
|
|
683
|
+
).all(since);
|
|
684
|
+
const trendCounts = {};
|
|
685
|
+
for (const row of trend) trendCounts[row.d] = row.c;
|
|
686
|
+
const recent = [];
|
|
687
|
+
for (let i = days - 1; i >= 0; i--) {
|
|
688
|
+
const d = new Date(now - i * 86400000).toISOString().slice(0, 10);
|
|
689
|
+
recent.push({ date: d, count: trendCounts[d] ?? 0 });
|
|
690
|
+
}
|
|
691
|
+
const total = Object.values(byTypeCounts).reduce((s, n) => s + n, 0);
|
|
692
|
+
return { byType: byTypeCounts, total, recent, days };
|
|
693
|
+
}
|
|
694
|
+
|
|
662
695
|
function getById(id) {
|
|
663
696
|
const row = db.prepare("SELECT * FROM memories WHERE id = ?").get(id);
|
|
664
697
|
return toRow(row);
|
|
@@ -2175,6 +2208,7 @@ export function createStore(path) {
|
|
|
2175
2208
|
return {
|
|
2176
2209
|
db,
|
|
2177
2210
|
count,
|
|
2211
|
+
stats,
|
|
2178
2212
|
getById,
|
|
2179
2213
|
save,
|
|
2180
2214
|
update,
|
|
@@ -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","title":"简短标题","content":"一句话内容","importance":1-5}。
|
|
4
|
+
只输出 JSON 数组,每项形如 {"type":"preference|project|decision|history|user|fact","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", "user", "fact"]);
|
|
21
21
|
return arr.filter(
|
|
22
22
|
(item) =>
|
|
23
23
|
item &&
|
|
@@ -192,7 +192,7 @@ export function createTools(ctx, service, config, embedder) {
|
|
|
192
192
|
"Call this when the user states a durable preference, a project decision is made, or a lesson is learned. " +
|
|
193
193
|
"Merges into an existing entry of the same type when the title matches.",
|
|
194
194
|
parameters: {
|
|
195
|
-
type: { type: "string", required: true, enum: ["preference", "project", "decision", "history"], description: "preference=user
|
|
195
|
+
type: { type: "string", required: true, enum: ["preference", "project", "decision", "history", "user", "fact"], description: "preference=user preference; project=project knowledge/state; decision=key decision; history=conversation summary; user=user profile (background/identity); fact=atomic factual statement" },
|
|
196
196
|
title: { type: "string", required: true, description: "Short unique title" },
|
|
197
197
|
content: { type: "string", required: true, description: "Memory body" },
|
|
198
198
|
tags: { type: "array", items: { type: "string" }, description: "Optional tags" },
|
|
@@ -266,7 +266,7 @@ export function createTools(ctx, service, config, embedder) {
|
|
|
266
266
|
name: "memory_list",
|
|
267
267
|
description: "List at most 50 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. The JSONL summary reports returned/shown/omitted, offset/total/next_offset, and each rendered entry's exact id, type, title, importance, updated_at, tags, and truncated content_preview.",
|
|
268
268
|
parameters: {
|
|
269
|
-
type: { type: "string", enum: ["preference", "project", "decision", "history"], description: "Filter by type; omit for all" },
|
|
269
|
+
type: { type: "string", enum: ["preference", "project", "decision", "history", "user", "fact"], description: "Filter by type; omit for all" },
|
|
270
270
|
limit: { type: "integer", description: "Page size (default and maximum 50; nonpositive values use 50)" },
|
|
271
271
|
offset: { type: "integer", description: "Page offset (default 0)" },
|
|
272
272
|
include_archived: { type: "boolean", description: "Include archived (hidden) entries so they can be found and restored (default false)" }
|
|
@@ -305,7 +305,7 @@ export function createTools(ctx, service, config, embedder) {
|
|
|
305
305
|
id: { type: "string", required: true, description: "Memory id" },
|
|
306
306
|
title: { type: "string" },
|
|
307
307
|
content: { type: "string" },
|
|
308
|
-
type: { type: "string", enum: ["preference", "project", "decision", "history"] },
|
|
308
|
+
type: { type: "string", enum: ["preference", "project", "decision", "history", "user", "fact"] },
|
|
309
309
|
tags: { type: "array", items: { type: "string" } },
|
|
310
310
|
importance: { type: "integer", description: "1-5" },
|
|
311
311
|
reason: { type: "string", description: "Optional context for the correction (what the user actually said/wanted), recorded for reflection" }
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { EventEmitter } from "node:events";
|
|
4
|
+
import { createStore } from "../src/store.js";
|
|
5
|
+
import { createService } from "../src/service.js";
|
|
6
|
+
import { createApi } from "../src/api.js";
|
|
7
|
+
import { createSettings } from "../src/settings.js";
|
|
8
|
+
import { TYPE_FILE } from "../src/mirror.js";
|
|
9
|
+
import { parseSummaryJson } from "../src/summarize.js";
|
|
10
|
+
|
|
11
|
+
// v0.8.x: layered memory types (user / fact) + Web panel stats endpoint.
|
|
12
|
+
// user = user profile (background/identity), fact = atomic factual statement.
|
|
13
|
+
// Both ride the existing single-table `memories` design — only the type enum
|
|
14
|
+
// and downstream lists (mirror / tools / summarize / inject) were extended.
|
|
15
|
+
|
|
16
|
+
class FakeRes extends EventEmitter {
|
|
17
|
+
constructor() { super(); this.statusCode = 200; this.body = ""; }
|
|
18
|
+
writeHead(code, headers) { this.statusCode = code; this.headers = headers; return this; }
|
|
19
|
+
end(text) { this.body = text ?? ""; this.emit("end"); return this; }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function req(path, method = "GET", body = null) {
|
|
23
|
+
const r = new EventEmitter();
|
|
24
|
+
r.url = path;
|
|
25
|
+
r.method = method;
|
|
26
|
+
r.headers = {};
|
|
27
|
+
if (body !== null) {
|
|
28
|
+
process.nextTick(() => {
|
|
29
|
+
r.emit("data", Buffer.from(JSON.stringify(body)));
|
|
30
|
+
r.emit("end");
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
return r;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function setup() {
|
|
37
|
+
const store = createStore(":memory:");
|
|
38
|
+
const settings = createSettings(store.db);
|
|
39
|
+
const service = createService({ store, mirror: null, config: {}, settings });
|
|
40
|
+
const commands = { add: () => {}, remove: () => {}, list: () => [] };
|
|
41
|
+
const routes = [];
|
|
42
|
+
const ctx = {
|
|
43
|
+
webServer: {
|
|
44
|
+
register(route) { routes.push(route); return () => {}; }
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
createApi(ctx, service, settings, commands, undefined, undefined, "");
|
|
48
|
+
const handler = (path) => routes.find((r) => r.path === path)?.handler;
|
|
49
|
+
return { store, service, handler };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
test("user/fact are valid memory types for saveWithDedupe", () => {
|
|
53
|
+
const { service } = setup();
|
|
54
|
+
const a = service.saveWithDedupe({ type: "user", title: "桉桉", content: "湖南工业大学学生,考研公共管理学", importance: 5 });
|
|
55
|
+
const b = service.saveWithDedupe({ type: "fact", title: "服务器地址", content: "云服务器 Ubuntu 22.04", importance: 3 });
|
|
56
|
+
assert.equal(a.memory.type, "user");
|
|
57
|
+
assert.equal(b.memory.type, "fact");
|
|
58
|
+
assert.equal(service.count("user"), 1);
|
|
59
|
+
assert.equal(service.count("fact"), 1);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("mirror TYPE_FILE covers user/fact layers", () => {
|
|
63
|
+
assert.equal(TYPE_FILE.user, "user.md");
|
|
64
|
+
assert.equal(TYPE_FILE.fact, "facts.md");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("autoSummarize accepts user/fact types", () => {
|
|
68
|
+
const parsed = parseSummaryJson(
|
|
69
|
+
'[{"type":"user","title":"用户城市","content":"湖南株洲","importance":4},{"type":"fact","title":"端口","content":"23334","importance":3}]'
|
|
70
|
+
);
|
|
71
|
+
assert.equal(parsed.length, 2);
|
|
72
|
+
assert.deepEqual(parsed.map((i) => i.type), ["user", "fact"]);
|
|
73
|
+
// legacy types still pass, unknown types rejected
|
|
74
|
+
const mixed = parseSummaryJson(
|
|
75
|
+
'[{"type":"preference","title":"语言","content":"中文","importance":2},{"type":"nonsense","title":"x","content":"y","importance":1}]'
|
|
76
|
+
);
|
|
77
|
+
assert.equal(mixed.length, 1);
|
|
78
|
+
assert.equal(mixed[0].type, "preference");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("GET /api/dsh-mneme/list?type=user filters the new layer", async () => {
|
|
82
|
+
const { service, handler } = setup();
|
|
83
|
+
service.saveWithDedupe({ type: "user", title: "桉桉", content: "用户画像", importance: 5 });
|
|
84
|
+
service.saveWithDedupe({ type: "fact", title: "事实", content: "原子事实", importance: 3 });
|
|
85
|
+
const res = new FakeRes();
|
|
86
|
+
await handler("/api/dsh-mneme/list")?.({ url: "/api/dsh-mneme/list?type=user" }, res);
|
|
87
|
+
const data = JSON.parse(res.body);
|
|
88
|
+
assert.equal(res.statusCode, 200);
|
|
89
|
+
assert.equal(data.total, 1);
|
|
90
|
+
assert.equal(data.items[0].type, "user");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("GET /api/dsh-mneme/stats returns byType distribution + recent trend", async () => {
|
|
94
|
+
const { service, handler } = setup();
|
|
95
|
+
service.saveWithDedupe({ type: "user", title: "桉桉", content: "用户画像", importance: 5 });
|
|
96
|
+
service.saveWithDedupe({ type: "fact", title: "事实1", content: "原子事实", importance: 3 });
|
|
97
|
+
service.saveWithDedupe({ type: "fact", title: "事实2", content: "另一个事实", importance: 2 });
|
|
98
|
+
service.saveWithDedupe({ type: "preference", title: "语言", content: "中文", importance: 2 });
|
|
99
|
+
const res = new FakeRes();
|
|
100
|
+
await handler("/api/dsh-mneme/stats")?.({ url: "/api/dsh-mneme/stats?days=7" }, res);
|
|
101
|
+
const data = JSON.parse(res.body);
|
|
102
|
+
assert.equal(res.statusCode, 200);
|
|
103
|
+
assert.equal(data.byType.user, 1);
|
|
104
|
+
assert.equal(data.byType.fact, 2);
|
|
105
|
+
assert.equal(data.byType.preference, 1);
|
|
106
|
+
assert.equal(data.total, 4);
|
|
107
|
+
// 7-day trend covers today (the only day with live rows)
|
|
108
|
+
assert.equal(data.recent.length, 7);
|
|
109
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
110
|
+
assert.equal(data.recent[6].date, today);
|
|
111
|
+
assert.equal(data.recent[6].count, 4);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("stats excludes archived/forgotten memories", async () => {
|
|
115
|
+
const { service, handler } = setup();
|
|
116
|
+
const a = service.saveWithDedupe({ type: "user", title: "桉桉", content: "用户画像", importance: 5 });
|
|
117
|
+
const b = service.saveWithDedupe({ type: "fact", title: "事实", content: "原子事实", importance: 3 });
|
|
118
|
+
service.setArchived(a.memory.id, true);
|
|
119
|
+
service.setForget(b.memory.id, true);
|
|
120
|
+
const res = new FakeRes();
|
|
121
|
+
await handler("/api/dsh-mneme/stats")?.({ url: "/api/dsh-mneme/stats?days=7" }, res);
|
|
122
|
+
const data = JSON.parse(res.body);
|
|
123
|
+
assert.equal(data.total, 0);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("stats clamps days to 1..30", async () => {
|
|
127
|
+
const { handler } = setup();
|
|
128
|
+
const res = new FakeRes();
|
|
129
|
+
await handler("/api/dsh-mneme/stats")?.({ url: "/api/dsh-mneme/stats?days=999" }, res);
|
|
130
|
+
const data = JSON.parse(res.body);
|
|
131
|
+
assert.equal(res.statusCode, 200);
|
|
132
|
+
assert.equal(data.recent.length, 30);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("stats coerces fractional days to integer (kimi S2)", async () => {
|
|
136
|
+
const { service, handler } = setup();
|
|
137
|
+
service.saveWithDedupe({ type: "fact", title: "事实", content: "原子事实", importance: 3 });
|
|
138
|
+
const res = new FakeRes();
|
|
139
|
+
await handler("/api/dsh-mneme/stats")?.({ url: "/api/dsh-mneme/stats?days=7.5" }, res);
|
|
140
|
+
const data = JSON.parse(res.body);
|
|
141
|
+
assert.equal(data.recent.length, 7); // floor(7.5) → 7, never a ragged 8
|
|
142
|
+
assert.equal(data.days, 7);
|
|
143
|
+
assert.equal(data.byType.fact, 1);
|
|
144
|
+
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@modusensus/dsh-mneme",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
3
|
+
"version": "0.7.5",
|
|
4
|
+
"description": "Structured memory engine for DeepSeek Harness. Offline semantic search, entity-attribute-timeline, autoDream self-consolidation, and human-editable Markdown storage.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -12,23 +12,16 @@
|
|
|
12
12
|
"url": "https://github.com/modusensus/dsh-mneme/issues"
|
|
13
13
|
},
|
|
14
14
|
"type": "module",
|
|
15
|
-
"main": "lib/index.js",
|
|
15
|
+
"main": "./dsh-mneme/lib/index.js",
|
|
16
16
|
"exports": {
|
|
17
17
|
".": {
|
|
18
|
-
"default": "./lib/index.js"
|
|
18
|
+
"default": "./dsh-mneme/lib/index.js"
|
|
19
19
|
},
|
|
20
20
|
"./client": {
|
|
21
|
-
"default": "./lib/client.js"
|
|
21
|
+
"default": "./dsh-mneme/lib/client.js"
|
|
22
22
|
},
|
|
23
|
-
"./package.json": "./package.json"
|
|
23
|
+
"./package.json": "./dsh-mneme/package.json"
|
|
24
24
|
},
|
|
25
|
-
"files": [
|
|
26
|
-
"lib",
|
|
27
|
-
"src",
|
|
28
|
-
"scripts",
|
|
29
|
-
"test",
|
|
30
|
-
"cordis.patch.yml"
|
|
31
|
-
],
|
|
32
25
|
"dsh": {
|
|
33
26
|
"client": {
|
|
34
27
|
"inject": [
|
|
@@ -40,9 +33,20 @@
|
|
|
40
33
|
"platform": "web"
|
|
41
34
|
},
|
|
42
35
|
"bundle": {
|
|
43
|
-
"patch": "./cordis.patch.yml"
|
|
36
|
+
"patch": "./dsh-mneme/cordis.patch.yml"
|
|
37
|
+
},
|
|
38
|
+
"marketplace": {
|
|
39
|
+
"profiles": [
|
|
40
|
+
"web"
|
|
41
|
+
],
|
|
42
|
+
"requiresBuildApproval": false,
|
|
43
|
+
"requiresRestart": true,
|
|
44
|
+
"manualSteps": false
|
|
44
45
|
}
|
|
45
46
|
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"prepublishOnly": "node -e \"const fs=require('fs');const v=JSON.parse(fs.readFileSync('./dsh-mneme/package.json','utf8')).version;const p=JSON.parse(fs.readFileSync('./package.json','utf8'));p.version=v;fs.writeFileSync('./package.json',JSON.stringify(p,null,2)+'\\n')\""
|
|
49
|
+
},
|
|
46
50
|
"peerDependencies": {
|
|
47
51
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
48
52
|
"@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
|
|
@@ -50,34 +54,5 @@
|
|
|
50
54
|
"@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.6",
|
|
51
55
|
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
|
|
52
56
|
"@deepseek-ai/schemastery": "^3.18.1"
|
|
53
|
-
},
|
|
54
|
-
"devDependencies": {
|
|
55
|
-
"@deepseek-ai/cordis": "^4.0.1",
|
|
56
|
-
"@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
|
|
57
|
-
"@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
|
|
58
|
-
"@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.6",
|
|
59
|
-
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
|
|
60
|
-
"@deepseek-ai/schemastery": "^3.18.1",
|
|
61
|
-
"c8": "^12.0.0"
|
|
62
|
-
},
|
|
63
|
-
"scripts": {
|
|
64
|
-
"sync": "node scripts/sync-lib.js",
|
|
65
|
-
"prepack": "npm run sync",
|
|
66
|
-
"test": "node --test test/*.test.js",
|
|
67
|
-
"test:coverage": "c8 node --test test/*.test.js",
|
|
68
|
-
"e2e": "node scripts/e2e-dsh.js",
|
|
69
|
-
"stress": "node scripts/stress-dsh.js"
|
|
70
|
-
},
|
|
71
|
-
"dependencies": {
|
|
72
|
-
"@huggingface/transformers": "^4.2.0"
|
|
73
|
-
},
|
|
74
|
-
"overrides": {
|
|
75
|
-
"adm-zip": "0.6.0"
|
|
76
|
-
},
|
|
77
|
-
"c8": {
|
|
78
|
-
"reporter": [
|
|
79
|
-
"text",
|
|
80
|
-
"lcov"
|
|
81
|
-
]
|
|
82
57
|
}
|
|
83
58
|
}
|
|
Binary file
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|