@modusensus/dsh-mneme 0.7.3 → 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 -419
- 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 +192 -3
- package/{lib → dsh-mneme/lib}/config.js +7 -0
- package/{lib → dsh-mneme/lib}/inject.js +20 -3
- 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}/config.js +7 -0
- package/{src → dsh-mneme/src}/inject.js +20 -3
- 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/{test → dsh-mneme/test}/config.test.js +7 -0
- package/{test → dsh-mneme/test}/inject.test.js +57 -0
- 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}/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}/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}/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}/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}/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}/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
|
@@ -65,6 +65,8 @@ window.__ModuleLoader__.load({
|
|
|
65
65
|
"memory.tab.project": "项目",
|
|
66
66
|
"memory.tab.decision": "决策",
|
|
67
67
|
"memory.tab.history": "历史",
|
|
68
|
+
"memory.tab.user": "用户",
|
|
69
|
+
"memory.tab.fact": "事实",
|
|
68
70
|
"memory.settings.title": "记忆库设置",
|
|
69
71
|
"memory.settings.profile": "用户画像",
|
|
70
72
|
"memory.settings.profileHint": "描述你自己(角色、背景、偏好),Agent 会在每轮遵循",
|
|
@@ -100,6 +102,7 @@ window.__ModuleLoader__.load({
|
|
|
100
102
|
"memory.explorer.tabGraph": "图谱",
|
|
101
103
|
"memory.explorer.tabDirectory": "目录",
|
|
102
104
|
"memory.explorer.tabSettings": "设置",
|
|
105
|
+
"memory.explorer.tabOverview": "总览",
|
|
103
106
|
"memory.explorer.search": "搜索标题或内容…",
|
|
104
107
|
"memory.explorer.searchTitle": "语义检索",
|
|
105
108
|
"memory.explorer.types": "分类",
|
|
@@ -122,6 +125,14 @@ window.__ModuleLoader__.load({
|
|
|
122
125
|
"memory.explorer.importance": "重要性",
|
|
123
126
|
"memory.explorer.topK": "返回数量",
|
|
124
127
|
"memory.explorer.topKOption": "返回 {n} 条",
|
|
128
|
+
"memory.overview.empty": "还没有记忆。开始对话后 Agent 会自动沉淀记忆,这里会展示分层总览",
|
|
129
|
+
"memory.overview.profileEmpty": "暂无用户画像记忆,可在对话中让 AI 记录",
|
|
130
|
+
"memory.overview.distribution": "类型分布",
|
|
131
|
+
"memory.overview.layers": "记忆分层",
|
|
132
|
+
"memory.overview.trend": "近 7 天趋势",
|
|
133
|
+
"memory.overview.none": "暂无",
|
|
134
|
+
"memory.overview.badgeInject": "常注入",
|
|
135
|
+
"memory.overview.badgeImportance": "按重要性",
|
|
125
136
|
"memory.directory.untagged": "无标签",
|
|
126
137
|
"memory.directory.loading": "加载中…",
|
|
127
138
|
"memory.directory.empty": "暂无记忆条目",
|
|
@@ -179,6 +190,8 @@ window.__ModuleLoader__.load({
|
|
|
179
190
|
"memory.tab.project": "Projects",
|
|
180
191
|
"memory.tab.decision": "Decisions",
|
|
181
192
|
"memory.tab.history": "History",
|
|
193
|
+
"memory.tab.user": "User",
|
|
194
|
+
"memory.tab.fact": "Facts",
|
|
182
195
|
"memory.settings.title": "Memory Settings",
|
|
183
196
|
"memory.settings.profile": "User Profile",
|
|
184
197
|
"memory.settings.profileHint": "Describe yourself — the agent follows this every turn",
|
|
@@ -214,6 +227,7 @@ window.__ModuleLoader__.load({
|
|
|
214
227
|
"memory.explorer.tabGraph": "Graph",
|
|
215
228
|
"memory.explorer.tabDirectory": "Directory",
|
|
216
229
|
"memory.explorer.tabSettings": "Settings",
|
|
230
|
+
"memory.explorer.tabOverview": "Overview",
|
|
217
231
|
"memory.explorer.search": "Search title or content…",
|
|
218
232
|
"memory.explorer.searchTitle": "Semantic Search",
|
|
219
233
|
"memory.explorer.types": "Types",
|
|
@@ -236,6 +250,14 @@ window.__ModuleLoader__.load({
|
|
|
236
250
|
"memory.explorer.importance": "Importance",
|
|
237
251
|
"memory.explorer.topK": "Results limit",
|
|
238
252
|
"memory.explorer.topKOption": "Return {n}",
|
|
253
|
+
"memory.overview.empty": "No memories yet. Once you chat, the agent starts saving memories and the layered overview appears here",
|
|
254
|
+
"memory.overview.profileEmpty": "No user-profile memories yet — ask the agent to remember things about you in chat",
|
|
255
|
+
"memory.overview.distribution": "By Type",
|
|
256
|
+
"memory.overview.layers": "Memory Layers",
|
|
257
|
+
"memory.overview.trend": "Last 7 days",
|
|
258
|
+
"memory.overview.none": "None",
|
|
259
|
+
"memory.overview.badgeInject": "Auto-inject",
|
|
260
|
+
"memory.overview.badgeImportance": "By importance",
|
|
239
261
|
"memory.directory.untagged": "Untagged",
|
|
240
262
|
"memory.directory.loading": "Loading…",
|
|
241
263
|
"memory.directory.empty": "No memories yet",
|
|
@@ -466,13 +488,43 @@ window.__ModuleLoader__.load({
|
|
|
466
488
|
// --- settings sub-view ---
|
|
467
489
|
".mneme-xsettings{flex:1;min-height:0;overflow-y:auto;padding:24px 24px 48px}",
|
|
468
490
|
".mneme-xsettings-inner{max-width:640px}",
|
|
491
|
+
// --- overview sub-view (layered stats dashboard, .mneme-ov-* only) ---
|
|
492
|
+
".mneme-ov-wrap{flex:1;min-height:0;overflow-y:auto;display:flex;flex-direction:column;gap:12px;padding:12px 16px 28px}",
|
|
493
|
+
".mneme-ov-profile{gap:8px}",
|
|
494
|
+
".mneme-ov-prow{display:flex;flex-direction:column;gap:2px;padding:4px 0;border-top:1px solid var(--dsw-alias-border-l1)}",
|
|
495
|
+
".mneme-ov-prow:first-of-type{border-top:none}",
|
|
496
|
+
".mneme-ov-ptitle{font-size:13px;font-weight:600;line-height:18px;color:var(--dsw-alias-label-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
497
|
+
".mneme-ov-ppreview{font-size:12px;line-height:17px;color:var(--dsw-alias-label-tertiary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
498
|
+
".mneme-ov-dist{display:flex;flex-direction:column;gap:6px}",
|
|
499
|
+
".mneme-ov-distrow{display:flex;align-items:center;gap:8px}",
|
|
500
|
+
".mneme-ov-distlabel{flex:none;width:76px;font-size:12px;line-height:16px;color:var(--dsw-alias-label-secondary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
501
|
+
".mneme-ov-distcount{flex:none;width:34px;font-size:12px;line-height:16px;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;text-align:right}",
|
|
502
|
+
".mneme-ov-disttrack{flex:1;min-width:0;height:8px;border-radius:4px;background:var(--dsw-alias-border-l2)}",
|
|
503
|
+
".mneme-ov-distbar{height:100%;min-width:2px;border-radius:4px;background:var(--dsw-alias-state-business-primary)}",
|
|
504
|
+
".mneme-ov-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(190px,1fr));gap:10px}",
|
|
505
|
+
".mneme-ov-tile{box-sizing:border-box;text-align:left;border:none;cursor:pointer;font-family:inherit;gap:6px;overflow:hidden}",
|
|
506
|
+
".mneme-ov-tile:hover{background:var(--dsw-alias-interactive-bg-hover)}",
|
|
507
|
+
".mneme-ov-tilehead{display:flex;align-items:center;gap:6px}",
|
|
508
|
+
".mneme-ov-tiletitle{min-width:0;font-size:13px;font-weight:600;line-height:18px;color:var(--dsw-alias-label-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
509
|
+
".mneme-ov-tilecount{flex:none;margin-left:auto;font-size:12px;line-height:16px;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums}",
|
|
510
|
+
".mneme-ov-badge{flex:none;font-size:11px;line-height:16px;padding:0 6px;border-radius:8px;background:color-mix(in srgb,var(--dsw-alias-state-business-primary) 12%,transparent);color:var(--dsw-alias-state-business-primary)}",
|
|
511
|
+
".mneme-ov-badge--dim{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-tertiary)}",
|
|
512
|
+
".mneme-ov-tilelatest{font-size:12px;line-height:17px;color:var(--dsw-alias-label-secondary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
|
|
513
|
+
".mneme-ov-trend{display:flex;align-items:flex-end;gap:8px;padding:8px 4px 0}",
|
|
514
|
+
".mneme-ov-trendcol{flex:1;min-width:0;display:flex;flex-direction:column;align-items:center;gap:4px}",
|
|
515
|
+
".mneme-ov-trendbar{width:min(100%,30px);border-radius:4px 4px 0 0;background:var(--dsw-alias-state-business-primary)}",
|
|
516
|
+
".mneme-ov-trenddate{font-size:10px;line-height:12px;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;white-space:nowrap}",
|
|
469
517
|
// --- hero fallback: full-viewport memory library when no tab ring exists ---
|
|
470
518
|
// The host hides the whole conversation tab ring while a session is
|
|
471
519
|
// blank (hero screen), so the sidebar entry cannot activate the tab
|
|
472
520
|
// there. This surface reuses the exact MemoryExplorer UI at full size —
|
|
473
521
|
// not a side drawer — so the library stays reachable from any state.
|
|
474
522
|
".mneme-overlay{position:fixed;inset:0;z-index:1000;display:flex;flex-direction:column;background:var(--dsw-alias-bg-layer-1);animation:mneme-fadein .12s ease-out}",
|
|
475
|
-
|
|
523
|
+
// issue #41: overlay 顶栏右侧原本与宿主窗口标题栏的控制按钮(最小化/最大化/关闭)
|
|
524
|
+
// 落在同一区域——宿主控制按钮浮在 web 内容最上层,抢占了 overlay"关闭"按钮的
|
|
525
|
+
// 点击热区,导致记忆窗口无法关闭。改为左对齐(标题 + 关闭按钮并排),关闭按钮
|
|
526
|
+
// 离开右上角宿主控制按钮区,任何窗口尺寸/宿主下都可见可点。
|
|
527
|
+
".mneme-overlaybar{flex:none;display:flex;align-items:center;justify-content:flex-start;gap:12px;height:44px;padding:0 12px 0 16px;border-bottom:1px solid var(--dsw-alias-border-l2)}",
|
|
476
528
|
".mneme-overlaytitle{font-size:14px;font-weight:600;color:var(--dsw-alias-label-primary)}",
|
|
477
529
|
".mneme-overlaybody{flex:1;min-height:0;display:flex;flex-direction:column;overflow:hidden}",
|
|
478
530
|
"@keyframes mneme-fadein{from{opacity:0}to{opacity:1}}"
|
|
@@ -1367,10 +1419,139 @@ window.__ModuleLoader__.load({
|
|
|
1367
1419
|
);
|
|
1368
1420
|
}
|
|
1369
1421
|
|
|
1370
|
-
|
|
1422
|
+
// --- Overview sub-view (layered stats dashboard) ---
|
|
1423
|
+
// Card grid of every memory type (fixed display order, strays appended
|
|
1424
|
+
// alphabetically), plus a user-profile card, a CSS-only type distribution
|
|
1425
|
+
// bar chart and a 7-day creation trend. Data comes from /stats; each tile
|
|
1426
|
+
// probes /list?type=X&limit=1 for a representative latest title (the list
|
|
1427
|
+
// API sorts importance-first, so limit=1 is the type's most prominent row).
|
|
1428
|
+
const OVERVIEW_TYPE_ORDER = ["preference", "project", "decision", "history", "user", "fact", "summary", "pattern"];
|
|
1429
|
+
// Injection-priority tiers from service.js (summary > preference/user >
|
|
1430
|
+
// importance): these layers ride into nearly every prompt.
|
|
1431
|
+
const OVERVIEW_INJECT_TYPES = ["summary", "user", "preference"];
|
|
1432
|
+
|
|
1433
|
+
function OverviewPanel({ t, onPickType }) {
|
|
1434
|
+
const [stats, setStats] = useState(null); // null = loading
|
|
1435
|
+
const [profile, setProfile] = useState(null); // user-type memories (list card)
|
|
1436
|
+
const [latest, setLatest] = useState({}); // type -> representative memory | null
|
|
1437
|
+
|
|
1438
|
+
useEffect(() => {
|
|
1439
|
+
let cancelled = false;
|
|
1440
|
+
apiFetch("/api/dsh-mneme/stats?days=7")
|
|
1441
|
+
.then((r) => (r.ok ? r.json() : null))
|
|
1442
|
+
.then((d) => { if (!cancelled) setStats(d && d.byType ? d : { byType: {}, total: 0, recent: [] }); })
|
|
1443
|
+
.catch(() => { if (!cancelled) setStats({ byType: {}, total: 0, recent: [] }); });
|
|
1444
|
+
apiFetch("/api/dsh-mneme/list?type=user&limit=20")
|
|
1445
|
+
.then((r) => (r.ok ? r.json() : { items: [] }))
|
|
1446
|
+
.then((d) => { if (!cancelled) setProfile(Array.isArray(d.items) ? d.items : []); })
|
|
1447
|
+
.catch(() => { if (!cancelled) setProfile([]); });
|
|
1448
|
+
return () => { cancelled = true; };
|
|
1449
|
+
}, []);
|
|
1450
|
+
|
|
1451
|
+
const byType = stats ? stats.byType : {};
|
|
1452
|
+
const gridTypes = stats
|
|
1453
|
+
? OVERVIEW_TYPE_ORDER.filter((k) => byType[k]).concat(
|
|
1454
|
+
Object.keys(byType).filter((k) => !OVERVIEW_TYPE_ORDER.includes(k)).sort())
|
|
1455
|
+
: [];
|
|
1456
|
+
|
|
1457
|
+
// Representative-latest probe, one small request per visible layer.
|
|
1458
|
+
// Derived purely from stats, so [stats] is the full dependency set.
|
|
1459
|
+
useEffect(() => {
|
|
1460
|
+
if (!stats || gridTypes.length === 0) return;
|
|
1461
|
+
let cancelled = false;
|
|
1462
|
+
Promise.all(gridTypes.map((k) =>
|
|
1463
|
+
apiFetch(`/api/dsh-mneme/list?type=${encodeURIComponent(k)}&limit=1`)
|
|
1464
|
+
.then((r) => (r.ok ? r.json() : { items: [] }))
|
|
1465
|
+
.then((d) => [k, (Array.isArray(d.items) && d.items[0]) || null])
|
|
1466
|
+
.catch(() => [k, null])
|
|
1467
|
+
)).then((pairs) => { if (!cancelled) setLatest(Object.fromEntries(pairs)); });
|
|
1468
|
+
return () => { cancelled = true; };
|
|
1469
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1470
|
+
}, [stats]);
|
|
1471
|
+
|
|
1472
|
+
if (!stats) return h("div", { className: "mneme-ov-wrap" }, h("div", { className: "mneme-hint" }, t("memory.directory.loading")));
|
|
1473
|
+
if (stats.total === 0) return h("div", { className: "mneme-ov-wrap" }, h("div", { className: "mneme-hint" }, t("memory.overview.empty")));
|
|
1474
|
+
|
|
1475
|
+
const distRow = (key) => {
|
|
1476
|
+
const count = byType[key] || 0;
|
|
1477
|
+
const pct = stats.total > 0 ? Math.round((count / stats.total) * 100) : 0;
|
|
1478
|
+
return h("div", { key, className: "mneme-ov-distrow" },
|
|
1479
|
+
h("span", { className: "mneme-ov-distlabel" }, typeLabel(t, key)),
|
|
1480
|
+
h("span", { className: "mneme-ov-distcount" }, String(count)),
|
|
1481
|
+
h("div", { className: "mneme-ov-disttrack" },
|
|
1482
|
+
h("div", { className: "mneme-ov-distbar", style: { width: `${pct}%` } }))
|
|
1483
|
+
);
|
|
1484
|
+
};
|
|
1485
|
+
|
|
1486
|
+
const tile = (key) => {
|
|
1487
|
+
const inject = OVERVIEW_INJECT_TYPES.includes(key);
|
|
1488
|
+
const last = latest[key];
|
|
1489
|
+
return h("button", {
|
|
1490
|
+
key,
|
|
1491
|
+
type: "button",
|
|
1492
|
+
className: "mneme-card mneme-ov-tile",
|
|
1493
|
+
title: t("memory.card.open"),
|
|
1494
|
+
onClick: () => onPickType && onPickType(key)
|
|
1495
|
+
},
|
|
1496
|
+
h("div", { className: "mneme-ov-tilehead" },
|
|
1497
|
+
h("span", { className: "mneme-ov-tiletitle" }, typeLabel(t, key)),
|
|
1498
|
+
h("span", { className: inject ? "mneme-ov-badge" : "mneme-ov-badge mneme-ov-badge--dim" },
|
|
1499
|
+
inject ? t("memory.overview.badgeInject") : t("memory.overview.badgeImportance")),
|
|
1500
|
+
h("span", { className: "mneme-ov-tilecount" }, String(byType[key] || 0))
|
|
1501
|
+
),
|
|
1502
|
+
h("div", { className: "mneme-ov-tilelatest" },
|
|
1503
|
+
(last && (last.title || (last.content || "").slice(0, 40))) || t("memory.overview.none"))
|
|
1504
|
+
);
|
|
1505
|
+
};
|
|
1506
|
+
|
|
1507
|
+
// Trend bars scale to the busiest day; heights are fixed px so they do
|
|
1508
|
+
// not depend on percentage resolution inside the flex columns.
|
|
1509
|
+
const recent = Array.isArray(stats.recent) ? stats.recent : [];
|
|
1510
|
+
const maxTrend = Math.max(1, ...recent.map((d) => d.count || 0));
|
|
1511
|
+
const trendCol = (d) => {
|
|
1512
|
+
const count = d.count || 0;
|
|
1513
|
+
const px = count > 0 ? 6 + Math.round((count / maxTrend) * 90) : 0;
|
|
1514
|
+
return h("div", {
|
|
1515
|
+
key: d.date,
|
|
1516
|
+
className: "mneme-ov-trendcol",
|
|
1517
|
+
title: `${d.date} · ${t("memory.explorer.count").replace("{n}", String(count))}`
|
|
1518
|
+
},
|
|
1519
|
+
h("div", { className: "mneme-ov-trendbar", style: { height: `${px}px` } }),
|
|
1520
|
+
h("div", { className: "mneme-ov-trenddate" }, (d.date || "").slice(5)) // MM-DD
|
|
1521
|
+
);
|
|
1522
|
+
};
|
|
1523
|
+
|
|
1524
|
+
return h("div", { className: "mneme-ov-wrap" },
|
|
1525
|
+
h("div", { className: "mneme-card mneme-ov-profile", role: "region", "aria-label": t("memory.settings.profile") },
|
|
1526
|
+
h("div", { className: "mneme-xcolhead" }, t("memory.settings.profile")),
|
|
1527
|
+
profile === null
|
|
1528
|
+
? h("div", { className: "mneme-ov-ppreview" }, t("memory.directory.loading"))
|
|
1529
|
+
: profile.length === 0
|
|
1530
|
+
? h("div", { className: "mneme-ov-ppreview" }, t("memory.overview.profileEmpty"))
|
|
1531
|
+
: profile.map((m) => h("div", { key: m.id, className: "mneme-ov-prow" },
|
|
1532
|
+
h("span", { className: "mneme-ov-ptitle" }, m.title || "—"),
|
|
1533
|
+
h("span", { className: "mneme-ov-ppreview" }, (m.content_preview || m.content || "").slice(0, 100))
|
|
1534
|
+
))
|
|
1535
|
+
),
|
|
1536
|
+
h("div", { className: "mneme-card", role: "region", "aria-label": t("memory.overview.distribution") },
|
|
1537
|
+
h("div", { className: "mneme-xcolhead" }, t("memory.overview.distribution")),
|
|
1538
|
+
h("div", { className: "mneme-ov-dist" }, gridTypes.map(distRow))
|
|
1539
|
+
),
|
|
1540
|
+
h("div", { className: "mneme-xcolhead" }, t("memory.overview.layers")),
|
|
1541
|
+
h("div", { className: "mneme-ov-grid" }, gridTypes.map(tile)),
|
|
1542
|
+
h("div", { className: "mneme-card", role: "region", "aria-label": t("memory.overview.trend") },
|
|
1543
|
+
h("div", { className: "mneme-xcolhead" }, t("memory.overview.trend")),
|
|
1544
|
+
recent.length === 0
|
|
1545
|
+
? h("div", { className: "mneme-ov-tilelatest" }, t("memory.overview.none"))
|
|
1546
|
+
: h("div", { className: "mneme-ov-trend" }, recent.map(trendCol))
|
|
1547
|
+
)
|
|
1548
|
+
);
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
const EXPLORER_TYPES = ["preference", "project", "decision", "summary", "history", "user", "fact"];
|
|
1371
1552
|
|
|
1372
1553
|
function MemoryExplorer({ t }) {
|
|
1373
|
-
const [view, setView] = useState("memory"); // memory | directory | graph | settings
|
|
1554
|
+
const [view, setView] = useState("memory"); // memory | overview | directory | graph | settings
|
|
1374
1555
|
const [items, setItems] = useState([]);
|
|
1375
1556
|
const [loading, setLoading] = useState(true);
|
|
1376
1557
|
const [type, setType] = useState("all");
|
|
@@ -1582,8 +1763,15 @@ window.__ModuleLoader__.load({
|
|
|
1582
1763
|
setView("graph");
|
|
1583
1764
|
};
|
|
1584
1765
|
|
|
1766
|
+
// Overview tile click: dive into the memory view filtered to that layer.
|
|
1767
|
+
const pickLayerType = (key) => {
|
|
1768
|
+
setType(key);
|
|
1769
|
+
setView("memory");
|
|
1770
|
+
};
|
|
1771
|
+
|
|
1585
1772
|
const subviews = [
|
|
1586
1773
|
{ key: "memory", label: t("memory.explorer.tabMemory") },
|
|
1774
|
+
{ key: "overview", label: t("memory.explorer.tabOverview") },
|
|
1587
1775
|
{ key: "directory", label: t("memory.explorer.tabDirectory") },
|
|
1588
1776
|
{ key: "graph", label: t("memory.explorer.tabGraph") },
|
|
1589
1777
|
{ key: "settings", label: t("memory.explorer.tabSettings") }
|
|
@@ -1772,6 +1960,7 @@ window.__ModuleLoader__.load({
|
|
|
1772
1960
|
)
|
|
1773
1961
|
)
|
|
1774
1962
|
),
|
|
1963
|
+
view === "overview" && h(OverviewPanel, { t, onPickType: pickLayerType }),
|
|
1775
1964
|
view === "directory" && h(DirectoryPanel, { t, onJump: jumpToMemory, collapsed: dirCollapsed, setCollapsed: setDirCollapsed }),
|
|
1776
1965
|
view === "graph" && h(GraphPanel, { t, focusEntity: graphFocus, onJumpMemory: jumpToMemory }),
|
|
1777
1966
|
view === "settings" && h("div", { className: "mneme-xsettings" },
|
|
@@ -22,6 +22,13 @@ export const Config = z.object({
|
|
|
22
22
|
// "现在几点/周几"。只注入一次(对话开始时),同会话后续渲染不再重复;
|
|
23
23
|
// 关闭时行为与之前完全一致。
|
|
24
24
|
injectTimePrefix: z.boolean().default(false),
|
|
25
|
+
// 花括号转义(issue #40,默认开):DSH 核心 interpolate() 会把 `{{name}}`
|
|
26
|
+
// 当 prompt 变量做严格校验(变量名须匹配 /^[a-z][a-z0-9_]*$/),记忆正文里
|
|
27
|
+
// 合法的模板语法(如 `{{hl|}}`、`{{挖空}}`、`{{关键词}}`)会因非法变量名
|
|
28
|
+
// 直接 throw、让整轮对话崩溃。开启后注入边界把 `{{`→`\{\{`、`}}`→`\}\}`,
|
|
29
|
+
// 文本不再含 `{{`/`}}` 子串,interpolate 不再扫描到,内容保持可读且幂等
|
|
30
|
+
// (不会二次转义)。关闭后按原样透传。
|
|
31
|
+
escapePromptVariables: z.boolean().default(true),
|
|
25
32
|
autoDream: z.boolean().default(true),
|
|
26
33
|
dreamThresholdCount: z.natural().min(1).max(1000).default(10),
|
|
27
34
|
dreamThresholdChars: z.natural().min(100).max(100000).default(5000),
|
|
@@ -75,6 +75,23 @@ export function createInjector(ctx, service, settings, config) {
|
|
|
75
75
|
const maxItems = config.maxInjectedItems ?? 5;
|
|
76
76
|
const threshold = config.importanceThreshold ?? 3;
|
|
77
77
|
|
|
78
|
+
// Prompt-variable escaping (issue #40): DSH's interpolate() treats `{{name}}`
|
|
79
|
+
// as a prompt variable and strictly validates the name against
|
|
80
|
+
// /^[a-z][a-z0-9_]*$/ — memory/profile content carrying legal template syntax
|
|
81
|
+
// (Obsidian-style `{{hl|}}`, `{{挖空}}`, `{{关键词}}`) would hit an illegal
|
|
82
|
+
// variable name and throw, crashing the whole turn. At the injection boundary
|
|
83
|
+
// we escape every run of 2+ consecutive braces, inserting a `\` between each
|
|
84
|
+
// pair, so no `{{`/`}}` substring survives into the prompt: `{{a}}` → `{\{a\}\}`,
|
|
85
|
+
// and odd runs like `{{{a}}}` (which pair-wise escaping would leave with a
|
|
86
|
+
// literal `{{`) are handled too. The text keeps its readable template form,
|
|
87
|
+
// the transform is idempotent (escaped braces are single + `\`, never two
|
|
88
|
+
// adjacent), and single braces pass through untouched — interpolate only
|
|
89
|
+
// scans `{{`. Off via config.escapePromptVariables=false (default true).
|
|
90
|
+
function escapePromptVars(text) {
|
|
91
|
+
if (config.escapePromptVariables === false) return String(text);
|
|
92
|
+
return String(text).replace(/[{}]{2,}/g, (run) => run.split("").join("\\"));
|
|
93
|
+
}
|
|
94
|
+
|
|
78
95
|
// Time-prefix injection (issue #34): opt-in, off by default. When enabled the
|
|
79
96
|
// current date/time is injected once per conversation — at the first prompt
|
|
80
97
|
// assembly of a new session — so the model can sense what time/day it is.
|
|
@@ -134,7 +151,7 @@ export function createInjector(ctx, service, settings, config) {
|
|
|
134
151
|
for (const r of rounds) hot.add(r);
|
|
135
152
|
const body = hot.getContext();
|
|
136
153
|
if (!body) return "";
|
|
137
|
-
return `[短期上下文] 最近对话(共 ${rounds.length} 轮):\n${body}
|
|
154
|
+
return escapePromptVars(`[短期上下文] 最近对话(共 ${rounds.length} 轮):\n${body}`);
|
|
138
155
|
}
|
|
139
156
|
|
|
140
157
|
function render(candidates) {
|
|
@@ -158,7 +175,7 @@ export function createInjector(ctx, service, settings, config) {
|
|
|
158
175
|
lines.push(`- [${m.type}] ${verified}${title}`);
|
|
159
176
|
}
|
|
160
177
|
}
|
|
161
|
-
return lines.join("\n");
|
|
178
|
+
return escapePromptVars(lines.join("\n"));
|
|
162
179
|
}
|
|
163
180
|
|
|
164
181
|
// Bug4: the system-prompt render is synchronous, so the semantic query vector
|
|
@@ -192,7 +209,7 @@ export function createInjector(ctx, service, settings, config) {
|
|
|
192
209
|
const lines = ["[用户设置] 来自 dsh-mneme 的用户画像与规则:"];
|
|
193
210
|
if (profile) lines.push(`- 用户画像:${profile}`);
|
|
194
211
|
for (const rule of rules) lines.push(`- 规则:${rule}`);
|
|
195
|
-
return lines.join("\n");
|
|
212
|
+
return escapePromptVars(lines.join("\n"));
|
|
196
213
|
}
|
|
197
214
|
|
|
198
215
|
const disposers = [
|
|
@@ -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" }
|