@modusensus/dsh-mneme 0.7.1 → 0.7.3

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.
Files changed (145) hide show
  1. package/README.md +418 -152
  2. package/{dsh-mneme/src → lib}/api.js +6 -3
  3. package/{dsh-mneme/lib → lib}/client.js +108 -9
  4. package/{dsh-mneme/src → lib}/config.js +15 -0
  5. package/{dsh-mneme/lib → lib}/inject.js +31 -2
  6. package/{dsh-mneme/src → lib}/service.js +14 -0
  7. package/{dsh-mneme/lib → lib}/settings.js +31 -0
  8. package/package.json +43 -18
  9. package/{dsh-mneme/lib → src}/api.js +6 -3
  10. package/{dsh-mneme/lib → src}/config.js +15 -0
  11. package/{dsh-mneme/src → src}/inject.js +31 -2
  12. package/{dsh-mneme/lib → src}/service.js +14 -0
  13. package/{dsh-mneme/src → src}/settings.js +31 -0
  14. package/{dsh-mneme/test → test}/api.test.js +27 -4
  15. package/{dsh-mneme/test → test}/client.test.js +57 -0
  16. package/{dsh-mneme/test → test}/config.test.js +14 -0
  17. package/{dsh-mneme/test → test}/inject.test.js +28 -0
  18. package/{dsh-mneme/test → test}/settings.test.js +22 -0
  19. package/.github/workflows/test.yml +0 -32
  20. package/CHANGELOG.md +0 -89
  21. package/CONTRIBUTING.md +0 -245
  22. package/SECURITY.md +0 -544
  23. package/docs/devlog/2026-08-14-dsh-mneme-dev-log.md +0 -247
  24. package/docs/devlog/2026-08-15-dsh-mneme-audit-stress-dev-log.md +0 -145
  25. package/docs/devlog/2026-08-15-dsh-mneme-pipeline-dev-log.md +0 -56
  26. package/docs/devlog/2026-08-15-dsh-mneme-reflection-dev-log.md +0 -77
  27. package/docs/devlog/2026-08-15-dsh-mneme-review-fixes-dev-log.md +0 -64
  28. package/docs/devlog/2026-08-15-dsh-mneme-semantic-dev-log.md +0 -90
  29. package/dsh-mneme/CHANGELOG.md +0 -295
  30. package/dsh-mneme/LICENSE +0 -21
  31. package/dsh-mneme/README.md +0 -484
  32. package/dsh-mneme/docs/AGENT_MEMORY_RESEARCH.md +0 -183
  33. package/dsh-mneme/docs/ENTITIES.md +0 -245
  34. package/dsh-mneme/docs/LOCAL_MODEL.md +0 -141
  35. package/dsh-mneme/docs/MIGRATION.md +0 -127
  36. package/dsh-mneme/docs/SEMANTIC.md +0 -256
  37. package/dsh-mneme/docs/SLEEP.md +0 -163
  38. package/dsh-mneme/package-lock.json +0 -1936
  39. package/dsh-mneme/package.json +0 -80
  40. package/v0.7-plan-2026-08-21.md +0 -75
  41. package//346/250/252/345/271/205.png +0 -0
  42. /package/{dsh-mneme/cordis.patch.yml → cordis.patch.yml} +0 -0
  43. /package/{dsh-mneme/lib → lib}/commands.js +0 -0
  44. /package/{dsh-mneme/lib → lib}/dream/clustering.js +0 -0
  45. /package/{dsh-mneme/lib → lib}/dream/decisions.js +0 -0
  46. /package/{dsh-mneme/lib → lib}/dream/sleep.js +0 -0
  47. /package/{dsh-mneme/lib → lib}/dream/tag-extractor.js +0 -0
  48. /package/{dsh-mneme/lib → lib}/dream.js +0 -0
  49. /package/{dsh-mneme/lib → lib}/embedding.js +0 -0
  50. /package/{dsh-mneme/lib → lib}/entities/extractor.js +0 -0
  51. /package/{dsh-mneme/lib → lib}/heat.js +0 -0
  52. /package/{dsh-mneme/lib → lib}/hot-memory.js +0 -0
  53. /package/{dsh-mneme/lib → lib}/index.js +0 -0
  54. /package/{dsh-mneme/lib → lib}/local-embedder.js +0 -0
  55. /package/{dsh-mneme/lib → lib}/mirror.js +0 -0
  56. /package/{dsh-mneme/lib → lib}/parser/tag.js +0 -0
  57. /package/{dsh-mneme/lib → lib}/parser/wiki-link.js +0 -0
  58. /package/{dsh-mneme/lib → lib}/quality-filter.js +0 -0
  59. /package/{dsh-mneme/lib → lib}/reranker.js +0 -0
  60. /package/{dsh-mneme/lib → lib}/search/adaptive.js +0 -0
  61. /package/{dsh-mneme/lib → lib}/search/bm25.js +0 -0
  62. /package/{dsh-mneme/lib → lib}/search/tag-boost.js +0 -0
  63. /package/{dsh-mneme/lib → lib}/store.js +0 -0
  64. /package/{dsh-mneme/lib → lib}/summarize.js +0 -0
  65. /package/{dsh-mneme/lib → lib}/tools.js +0 -0
  66. /package/{dsh-mneme/lib → lib}/vector-index.js +0 -0
  67. /package/{dsh-mneme/scripts → scripts}/benchmark-embed.js +0 -0
  68. /package/{dsh-mneme/scripts → scripts}/benchmark-recall.js +0 -0
  69. /package/{dsh-mneme/scripts → scripts}/benchmark-rerank.js +0 -0
  70. /package/{dsh-mneme/scripts → scripts}/e2e-dsh.js +0 -0
  71. /package/{dsh-mneme/scripts → scripts}/stress-dsh.js +0 -0
  72. /package/{dsh-mneme/scripts → scripts}/sync-lib.js +0 -0
  73. /package/{dsh-mneme/src → src}/commands.js +0 -0
  74. /package/{dsh-mneme/src → src}/dream/clustering.js +0 -0
  75. /package/{dsh-mneme/src → src}/dream/decisions.js +0 -0
  76. /package/{dsh-mneme/src → src}/dream/sleep.js +0 -0
  77. /package/{dsh-mneme/src → src}/dream/tag-extractor.js +0 -0
  78. /package/{dsh-mneme/src → src}/dream.js +0 -0
  79. /package/{dsh-mneme/src → src}/embedding.js +0 -0
  80. /package/{dsh-mneme/src → src}/entities/extractor.js +0 -0
  81. /package/{dsh-mneme/src → src}/heat.js +0 -0
  82. /package/{dsh-mneme/src → src}/hot-memory.js +0 -0
  83. /package/{dsh-mneme/src → src}/index.js +0 -0
  84. /package/{dsh-mneme/src → src}/local-embedder.js +0 -0
  85. /package/{dsh-mneme/src → src}/mirror.js +0 -0
  86. /package/{dsh-mneme/src → src}/parser/tag.js +0 -0
  87. /package/{dsh-mneme/src → src}/parser/wiki-link.js +0 -0
  88. /package/{dsh-mneme/src → src}/quality-filter.js +0 -0
  89. /package/{dsh-mneme/src → src}/reranker.js +0 -0
  90. /package/{dsh-mneme/src → src}/search/adaptive.js +0 -0
  91. /package/{dsh-mneme/src → src}/search/bm25.js +0 -0
  92. /package/{dsh-mneme/src → src}/search/tag-boost.js +0 -0
  93. /package/{dsh-mneme/src → src}/store.js +0 -0
  94. /package/{dsh-mneme/src → src}/summarize.js +0 -0
  95. /package/{dsh-mneme/src → src}/tools.js +0 -0
  96. /package/{dsh-mneme/src → src}/vector-index.js +0 -0
  97. /package/{dsh-mneme/test → test}/audit.test.js +0 -0
  98. /package/{dsh-mneme/test → test}/benchmark.test.js +0 -0
  99. /package/{dsh-mneme/test → test}/boundary-v0625.test.js +0 -0
  100. /package/{dsh-mneme/test → test}/clustering.test.js +0 -0
  101. /package/{dsh-mneme/test → test}/commands.test.js +0 -0
  102. /package/{dsh-mneme/test → test}/conflict-freeze.test.js +0 -0
  103. /package/{dsh-mneme/test → test}/directory.test.js +0 -0
  104. /package/{dsh-mneme/test → test}/dream.test.js +0 -0
  105. /package/{dsh-mneme/test → test}/entities.test.js +0 -0
  106. /package/{dsh-mneme/test → test}/epistemic.test.js +0 -0
  107. /package/{dsh-mneme/test → test}/fnew-0112.test.js +0 -0
  108. /package/{dsh-mneme/test → test}/fnew-03.test.js +0 -0
  109. /package/{dsh-mneme/test → test}/graph-api.test.js +0 -0
  110. /package/{dsh-mneme/test → test}/heat.test.js +0 -0
  111. /package/{dsh-mneme/test → test}/helpers/dream-mock.js +0 -0
  112. /package/{dsh-mneme/test → test}/hot-memory.test.js +0 -0
  113. /package/{dsh-mneme/test → test}/llm-audit.test.js +0 -0
  114. /package/{dsh-mneme/test → test}/local-embedder.test.js +0 -0
  115. /package/{dsh-mneme/test → test}/mirror-dirty.test.js +0 -0
  116. /package/{dsh-mneme/test → test}/mirror-edit-digest.test.js +0 -0
  117. /package/{dsh-mneme/test → test}/mirror-generation.test.js +0 -0
  118. /package/{dsh-mneme/test → test}/mirror.test.js +0 -0
  119. /package/{dsh-mneme/test → test}/normalize-decisions.test.js +0 -0
  120. /package/{dsh-mneme/test → test}/peer-blockers.test.js +0 -0
  121. /package/{dsh-mneme/test → test}/policy-epoch.test.js +0 -0
  122. /package/{dsh-mneme/test → test}/provenance.test.js +0 -0
  123. /package/{dsh-mneme/test → test}/quality-filter.test.js +0 -0
  124. /package/{dsh-mneme/test → test}/reasoning-effort.test.js +0 -0
  125. /package/{dsh-mneme/test → test}/recall-evals.test.js +0 -0
  126. /package/{dsh-mneme/test → test}/recall-layer.test.js +0 -0
  127. /package/{dsh-mneme/test → test}/recall-runs.test.js +0 -0
  128. /package/{dsh-mneme/test → test}/receipt-chain.test.js +0 -0
  129. /package/{dsh-mneme/test → test}/reflection.test.js +0 -0
  130. /package/{dsh-mneme/test → test}/reranker.test.js +0 -0
  131. /package/{dsh-mneme/test → test}/search-fusion.test.js +0 -0
  132. /package/{dsh-mneme/test → test}/semantic.test.js +0 -0
  133. /package/{dsh-mneme/test → test}/service-search.test.js +0 -0
  134. /package/{dsh-mneme/test → test}/service.test.js +0 -0
  135. /package/{dsh-mneme/test → test}/sleep-heat.test.js +0 -0
  136. /package/{dsh-mneme/test → test}/sleep.test.js +0 -0
  137. /package/{dsh-mneme/test → test}/store.test.js +0 -0
  138. /package/{dsh-mneme/test → test}/stress.test.js +0 -0
  139. /package/{dsh-mneme/test → test}/summarize.test.js +0 -0
  140. /package/{dsh-mneme/test → test}/tag-boost.test.js +0 -0
  141. /package/{dsh-mneme/test → test}/tag.test.js +0 -0
  142. /package/{dsh-mneme/test → test}/tools.test.js +0 -0
  143. /package/{dsh-mneme/test → test}/updated-at-semantics.test.js +0 -0
  144. /package/{dsh-mneme/test → test}/vector-index.test.js +0 -0
  145. /package/{dsh-mneme/test → test}/wiki-link.test.js +0 -0
@@ -238,19 +238,22 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
238
238
  }
239
239
  return true;
240
240
  };
241
- if (!setIfBoolean("autoTagEnabled") || !setIfBoolean("manualTagEnabled")) return;
241
+ if (!setIfBoolean("autoTagEnabled") || !setIfBoolean("manualTagEnabled") || !setIfBoolean("showSidebarTrigger")) return;
242
242
  if (Object.keys(patch).length === 0) {
243
243
  sendJson(res, 400, { error: "no valid config field provided" });
244
244
  return;
245
245
  }
246
+ // Each setter ignores the keys it doesn't own, so one patch can feed
247
+ // both stores safely (tag toggles → "autoTag", UI prefs → "ui").
246
248
  settings.setAutoTagConfig(patch);
249
+ settings.setUiConfig(patch);
247
250
  // Respond with the effective (settings-over-config merged) toggles
248
251
  // so the panel always sees booleans that actually gate runtime
249
252
  // behaviour (issue #31).
250
- sendJson(res, 200, { config: service.getTagConfig() });
253
+ sendJson(res, 200, { config: service.getAppConfig() });
251
254
  return;
252
255
  }
253
- sendJson(res, 200, { config: service.getTagConfig() });
256
+ sendJson(res, 200, { config: service.getAppConfig() });
254
257
  } catch {
255
258
  sendJson(res, 500, { error: "internal" });
256
259
  }
@@ -77,6 +77,11 @@ window.__ModuleLoader__.load({
77
77
  "memory.settings.autoTagEnabled": "自动打标签",
78
78
  "memory.settings.autoTagSave": "保存",
79
79
  "memory.settings.autoTagSaved": "已保存",
80
+ "memory.settings.sidebarTriggerTitle": "侧边栏入口按钮",
81
+ "memory.settings.sidebarTriggerHint": "关闭后隐藏侧边栏底部(左下角)的记忆入口按钮。与其他插件 UI 冲突时建议关闭;记忆库仍可通过顶部「记忆库」标签访问",
82
+ "memory.settings.sidebarTriggerEnabled": "显示左下角入口按钮",
83
+ "memory.settings.sidebarTriggerSave": "保存",
84
+ "memory.settings.sidebarTriggerSaved": "已保存",
80
85
  "memory.settings.ruleAdd": "添加规则",
81
86
  "memory.settings.rulePlaceholder": "例如:回答时总是先给结论",
82
87
  "memory.settings.commands": "自定义指令",
@@ -122,6 +127,8 @@ window.__ModuleLoader__.load({
122
127
  "memory.directory.empty": "暂无记忆条目",
123
128
  "directory.editToggle": "编辑模式(显示删除)",
124
129
  "directory.deleteConfirm": "确定删除这条记忆吗?",
130
+ "directory.deleteConfirmShort": "确认?",
131
+ "directory.deleteError": "删除失败",
125
132
  "memory.card.open": "在主区记忆库中查看全文",
126
133
  "memory.time.now": "刚刚",
127
134
  "memory.time.seconds": "{n}秒前",
@@ -184,6 +191,11 @@ window.__ModuleLoader__.load({
184
191
  "memory.settings.autoTagEnabled": "Auto-tagging",
185
192
  "memory.settings.autoTagSave": "Save",
186
193
  "memory.settings.autoTagSaved": "Saved",
194
+ "memory.settings.sidebarTriggerTitle": "Sidebar trigger",
195
+ "memory.settings.sidebarTriggerHint": "Hide the memory entrance button at the sidebar footer (bottom-left) when it clashes with other plugins. The library stays reachable via the 记忆库 conversation tab",
196
+ "memory.settings.sidebarTriggerEnabled": "Show the bottom-left trigger",
197
+ "memory.settings.sidebarTriggerSave": "Save",
198
+ "memory.settings.sidebarTriggerSaved": "Saved",
187
199
  "memory.settings.ruleAdd": "Add Rule",
188
200
  "memory.settings.rulePlaceholder": "e.g. Always lead with a conclusion",
189
201
  "memory.settings.commands": "Custom Commands",
@@ -229,6 +241,8 @@ window.__ModuleLoader__.load({
229
241
  "memory.directory.empty": "No memories yet",
230
242
  "directory.editToggle": "Edit mode",
231
243
  "directory.deleteConfirm": "Delete this memory?",
244
+ "directory.deleteConfirmShort": "Confirm?",
245
+ "directory.deleteError": "Delete failed",
232
246
  "memory.card.open": "Open full text in the Memory tab",
233
247
  "memory.time.now": "just now",
234
248
  "memory.time.seconds": "{n}s ago",
@@ -432,6 +446,9 @@ window.__ModuleLoader__.load({
432
446
  ".mneme-dir-del{margin-left:auto;background:none;border:none;color:#e5484d;cursor:pointer;font-size:13px;line-height:1;padding:2px 4px}",
433
447
  ".mneme-dir-del:hover{filter:brightness(1.25)}",
434
448
  ".mneme-edit-on .mneme-dir-del{opacity:1}",
449
+ ".mneme-dir-del-confirm{font-weight:700;background:rgba(229,72,77,.12);border-radius:4px}",
450
+ ".mneme-dir-item.mneme-dir-error{background:rgba(229,72,77,.14)}",
451
+ ".mneme-dir-del-err{flex:none;font-size:12px;line-height:16px;color:#e5484d}",
435
452
  ".mneme-dir-ititle{background:none;border:none;padding:0;font:inherit;color:inherit;cursor:pointer;flex:1;text-align:left;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
436
453
  ".mneme-dir-folder{margin-bottom:4px}",
437
454
  ".mneme-dir-folderhead{display:flex;align-items:center;gap:6px;width:100%;padding:6px 8px;border:none;border-radius:8px;background:none;color:var(--dsw-alias-label-primary);cursor:pointer;font-family:inherit;font-size:13px;font-weight:500;line-height:18px;text-align:left}",
@@ -867,6 +884,8 @@ window.__ModuleLoader__.load({
867
884
  const [apiTokenSaved, setApiTokenSaved] = react.useState(false);
868
885
  const [autoTag, setAutoTag] = react.useState(false);
869
886
  const [autoTagSaved, setAutoTagSaved] = react.useState(false);
887
+ const [showSidebarTrigger, setShowSidebarTrigger] = react.useState(true);
888
+ const [sidebarTriggerSaved, setSidebarTriggerSaved] = react.useState(false);
870
889
 
871
890
  const load = react.useCallback(async () => {
872
891
  try {
@@ -884,7 +903,9 @@ window.__ModuleLoader__.load({
884
903
  try {
885
904
  const g = await apiFetch("/api/dsh-mneme/config").then((res) => res.json());
886
905
  setAutoTag(g.config?.autoTagEnabled === true);
887
- } catch { /* keep default off */ }
906
+ // issue #38: sidebar trigger defaults to visible (true).
907
+ setShowSidebarTrigger(g.config?.showSidebarTrigger !== false);
908
+ } catch { /* keep defaults */ }
888
909
  } catch { /* ignore */ }
889
910
  }, []);
890
911
 
@@ -993,6 +1014,18 @@ window.__ModuleLoader__.load({
993
1014
  } catch { /* ignore */ }
994
1015
  }
995
1016
 
1017
+ async function saveSidebarTrigger() {
1018
+ try {
1019
+ await apiFetch("/api/dsh-mneme/config", {
1020
+ method: "PUT",
1021
+ headers: { "Content-Type": "application/json" },
1022
+ body: JSON.stringify({ showSidebarTrigger })
1023
+ });
1024
+ setSidebarTriggerSaved(true);
1025
+ setTimeout(() => setSidebarTriggerSaved(false), 1500);
1026
+ } catch { /* ignore */ }
1027
+ }
1028
+
996
1029
  const inputStyle = { boxSizing: "border-box", width: "100%", height: 34, padding: "0 12px", borderRadius: 10, border: "1px solid var(--dsw-alias-border-l2, #ddd)", background: "var(--dsw-alias-bg-base, transparent)", color: "var(--dsw-alias-label-primary)", fontFamily: "inherit", fontSize: 13, outline: "none", marginBottom: 8 };
997
1030
  const labelStyle = { fontSize: 12, fontWeight: 600, margin: "12px 0 4px", color: "var(--dsw-alias-label-primary)" };
998
1031
  const hintStyle = { fontSize: 11, color: "var(--dsw-alias-label-tertiary, #999)", marginBottom: 8 };
@@ -1070,6 +1103,17 @@ window.__ModuleLoader__.load({
1070
1103
  h("button", { style: styles.footerButton, onClick: saveAutoTag }, t("memory.settings.autoTagSave")),
1071
1104
  autoTagSaved && h("span", { style: { fontSize: 12, color: "var(--dsw-alias-state-success, #2a7)" } }, t("memory.settings.autoTagSaved"))
1072
1105
  ),
1106
+ // sidebar trigger switch (issue #38)
1107
+ h("div", { style: { ...labelStyle, marginTop: 20 } }, t("memory.settings.sidebarTriggerTitle")),
1108
+ h("div", { style: hintStyle }, t("memory.settings.sidebarTriggerHint")),
1109
+ h("label", { style: { display: "flex", alignItems: "center", gap: 6, marginBottom: 8, fontSize: 13 } },
1110
+ h("input", { type: "checkbox", checked: showSidebarTrigger, onChange: (e) => setShowSidebarTrigger(e.target.checked) }),
1111
+ h("span", null, t("memory.settings.sidebarTriggerEnabled"))
1112
+ ),
1113
+ h("div", { style: { display: "flex", alignItems: "center", gap: 6 } },
1114
+ h("button", { style: styles.footerButton, onClick: saveSidebarTrigger }, t("memory.settings.sidebarTriggerSave")),
1115
+ sidebarTriggerSaved && h("span", { style: { fontSize: 12, color: "var(--dsw-alias-state-success, #2a7)" } }, t("memory.settings.sidebarTriggerSaved"))
1116
+ ),
1073
1117
  // vector search
1074
1118
  h("div", { style: { ...labelStyle, marginTop: 20 } }, t("memory.settings.vectorTitle")),
1075
1119
  h("div", { style: hintStyle }, t("memory.settings.vectorHint")),
@@ -1184,6 +1228,9 @@ window.__ModuleLoader__.load({
1184
1228
  const [dir, setDir] = useState(null); // { groups: [...], untagged: [...] }
1185
1229
  const [status, setStatus] = useState("loading"); // loading | ready | error
1186
1230
  const [editMode, setEditMode] = useState(false); // manual tag/delete UI gate
1231
+ const [confirmingId, setConfirmingId] = useState(null); // row waiting for the 2nd confirm click
1232
+ const [delErrorId, setDelErrorId] = useState(null); // row whose delete just failed
1233
+ const confirmTimer = useRef(null);
1187
1234
 
1188
1235
  useEffect(() => {
1189
1236
  let cancelled = false;
@@ -1215,9 +1262,24 @@ window.__ModuleLoader__.load({
1215
1262
  }).then((res) => { if (res.ok) setEditMode(next); }).catch(() => {});
1216
1263
  };
1217
1264
 
1265
+ const resetConfirm = () => {
1266
+ clearTimeout(confirmTimer.current);
1267
+ setConfirmingId(null);
1268
+ };
1269
+
1218
1270
  const handleDelete = (memId, e) => {
1219
1271
  e?.stopPropagation?.();
1220
- if (typeof window === "undefined" || !window.confirm(t("directory.deleteConfirm"))) return;
1272
+ // 第一击进入「确认?」态,第二击才真正发 DELETE;宿主 web 环境里
1273
+ // window.confirm 不可靠(可能被拦截或直接返回 false),点击会无反应,
1274
+ // 所以改为面板内联两步确认,不依赖任何原生对话框。
1275
+ if (confirmingId !== memId) {
1276
+ setConfirmingId(memId);
1277
+ clearTimeout(confirmTimer.current);
1278
+ confirmTimer.current = setTimeout(() => setConfirmingId((cur) => (cur === memId ? null : cur)), 3000);
1279
+ return;
1280
+ }
1281
+ clearTimeout(confirmTimer.current);
1282
+ setConfirmingId(null);
1221
1283
  apiFetch("/api/dsh-mneme/memories", {
1222
1284
  method: "DELETE",
1223
1285
  headers: { "Content-Type": "application/json" },
@@ -1225,26 +1287,48 @@ window.__ModuleLoader__.load({
1225
1287
  })
1226
1288
  .then((res) => (res.ok ? res.json() : null))
1227
1289
  .then((d) => {
1228
- if (!d || d.deleted !== true) return;
1290
+ if (!d || d.deleted !== true) {
1291
+ // 服务端未确认删除也给出可见提示,不静默吞掉
1292
+ setDelErrorId(memId);
1293
+ setTimeout(() => setDelErrorId((cur) => (cur === memId ? null : cur)), 3500);
1294
+ return;
1295
+ }
1229
1296
  setDir((prev) => ({
1230
1297
  groups: prev.groups.map((g) => ({ ...g, memories: g.memories.filter((m) => m.id !== memId) })),
1231
1298
  untagged: prev.untagged.filter((m) => m.id !== memId)
1232
1299
  }));
1233
1300
  })
1234
- .catch(() => {});
1301
+ .catch(() => {
1302
+ // 网络失败同样给出可见提示
1303
+ setDelErrorId(memId);
1304
+ setTimeout(() => setDelErrorId((cur) => (cur === memId ? null : cur)), 3500);
1305
+ });
1235
1306
  };
1236
1307
 
1237
1308
  const renderFolder = (name, memories, extraKey, emptyLabel) => {
1238
1309
  const folderKey = extraKey || name;
1239
1310
  const open = !collapsed[folderKey];
1240
- const rows = memories.map((m) =>
1241
- h("div", { key: m.id, className: "mneme-dir-item" },
1311
+ const rows = memories.map((m) => {
1312
+ const confirming = confirmingId === m.id;
1313
+ const err = delErrorId === m.id;
1314
+ return h("div", {
1315
+ key: m.id,
1316
+ className: err ? "mneme-dir-item mneme-dir-error" : "mneme-dir-item",
1317
+ // 点击行内其它地方取消「确认?」态
1318
+ onClick: resetConfirm
1319
+ },
1242
1320
  h("span", { className: "mneme-tree-line" }),
1243
1321
  h("button", { type: "button", className: "mneme-dir-ititle", onClick: () => onJump(m) }, m.title || m.content?.slice(0, 40)),
1244
1322
  h("span", { className: "mneme-dir-itime" }, formatDate(m.updated_at || m.created_at)),
1245
- editMode && h("button", { type: "button", className: "mneme-dir-del", "aria-label": "delete", onClick: (e) => handleDelete(m.id, e) }, "✕")
1246
- )
1247
- );
1323
+ editMode && h("button", {
1324
+ type: "button",
1325
+ className: confirming ? "mneme-dir-del mneme-dir-del-confirm" : "mneme-dir-del",
1326
+ "aria-label": "delete",
1327
+ onClick: (e) => handleDelete(m.id, e)
1328
+ }, confirming ? t("directory.deleteConfirmShort") : "✕"),
1329
+ err && h("span", { className: "mneme-dir-del-err" }, t("directory.deleteError"))
1330
+ );
1331
+ });
1248
1332
  return h("div", {
1249
1333
  key: folderKey,
1250
1334
  className: "mneme-dir-folder",
@@ -1702,7 +1786,22 @@ window.__ModuleLoader__.load({
1702
1786
  // opens the full-viewport overlay instead — the library stays reachable
1703
1787
  // from every conversation state.
1704
1788
  function SidebarTrigger({ wide, t }) {
1789
+ // issue #38: the entrance button is now optional (settings-over-config
1790
+ // toggle, persisted via /api/dsh-mneme/config). While the value loads we
1791
+ // render the button (default = visible) so it never flashes out; if the
1792
+ // user disabled it, it drops away after the config resolves. The library
1793
+ // itself is unaffected — the 记忆库 conversation tab stays.
1794
+ const [visible, setVisible] = useState(true);
1795
+ useEffect(() => {
1796
+ let cancelled = false;
1797
+ apiFetch("/api/dsh-mneme/config")
1798
+ .then((res) => res.json())
1799
+ .then((d) => { if (!cancelled) setVisible(d.config?.showSidebarTrigger !== false); })
1800
+ .catch(() => { if (!cancelled) setVisible(true); });
1801
+ return () => { cancelled = true; };
1802
+ }, []);
1705
1803
  const [, setOpen] = useOverlayOpen();
1804
+ if (!visible) return null;
1706
1805
  return h("button", {
1707
1806
  type: "button",
1708
1807
  className: wide ? "mneme-trigger" : "mneme-trigger mneme-rail",
@@ -17,6 +17,11 @@ export const Config = z.object({
17
17
  summarizeModel: z.string().default(""),
18
18
  maxInjectedItems: z.natural().min(1).max(20).default(5),
19
19
  importanceThreshold: z.natural().min(1).max(5).default(3),
20
+ // 时间前缀注入(issue #34,opt-in,默认关):开启时,新对话开始时把当前
21
+ // 日期时间以 [当前时间: 2026-08-26 周二 19:30] 前缀注入一次,让模型感知
22
+ // "现在几点/周几"。只注入一次(对话开始时),同会话后续渲染不再重复;
23
+ // 关闭时行为与之前完全一致。
24
+ injectTimePrefix: z.boolean().default(false),
20
25
  autoDream: z.boolean().default(true),
21
26
  dreamThresholdCount: z.natural().min(1).max(1000).default(10),
22
27
  dreamThresholdChars: z.natural().min(100).max(100000).default(5000),
@@ -198,6 +203,16 @@ export const Config = z.object({
198
203
  // set false to disable the manual write path too.
199
204
  manualTagEnabled: z.boolean().default(true),
200
205
 
206
+ // --- sidebar trigger (issue #38) ---------------------------------------
207
+ // The memory library is reachable from two UI surfaces: a "记忆库" tab in the
208
+ // conversation header AND an entrance button at the sidebar footer (bottom
209
+ // left). Some sidebars get crowded there — dsh-cost-meter and friends also
210
+ // claim the footer slot — so the entrance button is now optional. Off just
211
+ // hides the button; the conversation tab stays, so no functionality is lost.
212
+ // The Web panel exposes this as a toggle (settings-over-config, see
213
+ // settings.getUiConfig); the value here is the plugin-config default.
214
+ showSidebarTrigger: z.boolean().default(true),
215
+
201
216
  // --- tag-weighted re-rank (v0.6.4) -------------------------------------
202
217
  // Opt-in: boost candidates whose tags overlap the query/session tags.
203
218
  tagBoostEnabled: z.boolean().default(false),
@@ -75,6 +75,32 @@ export function createInjector(ctx, service, settings, config) {
75
75
  const maxItems = config.maxInjectedItems ?? 5;
76
76
  const threshold = config.importanceThreshold ?? 3;
77
77
 
78
+ // Time-prefix injection (issue #34): opt-in, off by default. When enabled the
79
+ // current date/time is injected once per conversation — at the first prompt
80
+ // assembly of a new session — so the model can sense what time/day it is.
81
+ // A per-session latch means later assemblies in the same session never
82
+ // re-inject; a bare render ctx (no session id) falls back to once per
83
+ // injector lifetime.
84
+ const WEEKDAYS = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"];
85
+ let timePrefixSession = null;
86
+
87
+ function renderTimePrefix(ctx) {
88
+ if (config.injectTimePrefix !== true) return "";
89
+ const sessionId = ctx?.agent?.session?.id;
90
+ if (sessionId === undefined) {
91
+ if (timePrefixSession !== null) return "";
92
+ timePrefixSession = true;
93
+ } else {
94
+ if (sessionId === timePrefixSession) return "";
95
+ timePrefixSession = sessionId;
96
+ }
97
+ const now = new Date();
98
+ const pad = (n) => String(n).padStart(2, "0");
99
+ const date = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
100
+ const time = `${pad(now.getHours())}:${pad(now.getMinutes())}`;
101
+ return `[当前时间: ${date} ${WEEKDAYS[now.getDay()]} ${time}]`;
102
+ }
103
+
78
104
  // Bug6: bound the injected memory block. Each entry's content is truncated to
79
105
  // MAX_CONTENT chars (trailing `…`); the whole block gets a MAX_BLOCK budget
80
106
  // and an entry that would exceed it collapses to its title only, so a long
@@ -188,8 +214,11 @@ export function createInjector(ctx, service, settings, config) {
188
214
  // separate context) keeps the prompt assembly stable at two blocks.
189
215
  const hotText = renderHotContext(ctx);
190
216
  const body = render(candidates);
191
- if (!hotText) return body;
192
- return body ? `${hotText}\n\n${body}` : hotText;
217
+ let out = !hotText ? body : (body ? `${hotText}\n\n${body}` : hotText);
218
+ // Time prefix (issue #34): leads the injection block once per session.
219
+ const timePrefix = renderTimePrefix(ctx);
220
+ if (!timePrefix) return out;
221
+ return out ? `${timePrefix}\n\n${out}` : timePrefix;
193
222
  }
194
223
  }),
195
224
  ctx.systemPrompt.context({
@@ -105,6 +105,19 @@ export function createService({ store, mirror, config, onWrite, logger, settings
105
105
  };
106
106
  };
107
107
 
108
+ // App-wide effective config for the Web panel (/api/dsh-mneme/config): the
109
+ // tag toggles plus UI preferences (issue #38). Settings overrides plugin
110
+ // config when explicitly stored; unset keys fall back to the zod default.
111
+ const appConfig = () => {
112
+ const stored = settings?.getUiConfig?.() ?? {};
113
+ return {
114
+ ...tagConfig(),
115
+ showSidebarTrigger: typeof stored.showSidebarTrigger === "boolean"
116
+ ? stored.showSidebarTrigger
117
+ : (config.showSidebarTrigger ?? true)
118
+ };
119
+ };
120
+
108
121
  // Optional recall recorder, installed via setRecallRecorder after creation.
109
122
  // When searchMemories is called with recordRecall=true it receives the
110
123
  // actual merged recall scene (candidates + scores + source + threshold) so
@@ -1803,6 +1816,7 @@ export function createService({ store, mirror, config, onWrite, logger, settings
1803
1816
  // Effective tag toggle pair (settings-over-config merge) for the panel
1804
1817
  // (/api/dsh-mneme/config GET/PUT).
1805
1818
  getTagConfig: tagConfig,
1819
+ getAppConfig: appConfig,
1806
1820
  applyMemoryTags: (memoryId, tags) => store.setMemoryTags(memoryId, tags),
1807
1821
  saveAttr: (r) => store.saveAttr(r),
1808
1822
  createEntity: (r) => store.createEntity(r),
@@ -176,6 +176,37 @@ export function createSettings(db) {
176
176
  else if (cur.manualTagEnabled !== undefined) cfg.manualTagEnabled = cur.manualTagEnabled === true;
177
177
  setSetting("autoTag", JSON.stringify(cfg));
178
178
  return cfg;
179
+ },
180
+ /** UI preferences (issue #38): same settings-over-config pattern as the
181
+ * tag toggles. null = never explicitly stored → callers fall back to the
182
+ * plugin-config default, so a partial update can't invent a value for a
183
+ * key the user never touched. */
184
+ getUiConfig() {
185
+ const raw = getSetting("ui");
186
+ let stored = {};
187
+ if (raw) {
188
+ try {
189
+ const j = JSON.parse(raw);
190
+ if (j && typeof j === "object") stored = j;
191
+ } catch { /* fall through to defaults */ }
192
+ }
193
+ return {
194
+ showSidebarTrigger: typeof stored.showSidebarTrigger === "boolean" ? stored.showSidebarTrigger : null
195
+ };
196
+ },
197
+ setUiConfig(partial) {
198
+ const cur = (() => {
199
+ const raw = getSetting("ui");
200
+ try {
201
+ const j = JSON.parse(raw);
202
+ return j && typeof j === "object" ? j : {};
203
+ } catch { return {}; }
204
+ })();
205
+ const cfg = {};
206
+ if (partial.showSidebarTrigger !== undefined) cfg.showSidebarTrigger = partial.showSidebarTrigger === true;
207
+ else if (cur.showSidebarTrigger !== undefined) cfg.showSidebarTrigger = cur.showSidebarTrigger === true;
208
+ setSetting("ui", JSON.stringify(cfg));
209
+ return cfg;
179
210
  }
180
211
  };
181
212
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@modusensus/dsh-mneme",
3
- "version": "0.7.1",
4
- "description": "Structured memory engine for DeepSeek Harness. Offline semantic search, entity-attribute-timeline, autoDream self-consolidation, and human-editable Markdown storage.",
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.3",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -12,16 +12,23 @@
12
12
  "url": "https://github.com/modusensus/dsh-mneme/issues"
13
13
  },
14
14
  "type": "module",
15
- "main": "./dsh-mneme/lib/index.js",
15
+ "main": "lib/index.js",
16
16
  "exports": {
17
17
  ".": {
18
- "default": "./dsh-mneme/lib/index.js"
18
+ "default": "./lib/index.js"
19
19
  },
20
20
  "./client": {
21
- "default": "./dsh-mneme/lib/client.js"
21
+ "default": "./lib/client.js"
22
22
  },
23
- "./package.json": "./dsh-mneme/package.json"
23
+ "./package.json": "./package.json"
24
24
  },
25
+ "files": [
26
+ "lib",
27
+ "src",
28
+ "scripts",
29
+ "test",
30
+ "cordis.patch.yml"
31
+ ],
25
32
  "dsh": {
26
33
  "client": {
27
34
  "inject": [
@@ -33,20 +40,9 @@
33
40
  "platform": "web"
34
41
  },
35
42
  "bundle": {
36
- "patch": "./dsh-mneme/cordis.patch.yml"
37
- },
38
- "marketplace": {
39
- "profiles": [
40
- "web"
41
- ],
42
- "requiresBuildApproval": false,
43
- "requiresRestart": true,
44
- "manualSteps": false
43
+ "patch": "./cordis.patch.yml"
45
44
  }
46
45
  },
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
- },
50
46
  "peerDependencies": {
51
47
  "@deepseek-ai/cordis": "^4.0.1",
52
48
  "@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
@@ -54,5 +50,34 @@
54
50
  "@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.6",
55
51
  "@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
56
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
+ ]
57
82
  }
58
83
  }
@@ -238,19 +238,22 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
238
238
  }
239
239
  return true;
240
240
  };
241
- if (!setIfBoolean("autoTagEnabled") || !setIfBoolean("manualTagEnabled")) return;
241
+ if (!setIfBoolean("autoTagEnabled") || !setIfBoolean("manualTagEnabled") || !setIfBoolean("showSidebarTrigger")) return;
242
242
  if (Object.keys(patch).length === 0) {
243
243
  sendJson(res, 400, { error: "no valid config field provided" });
244
244
  return;
245
245
  }
246
+ // Each setter ignores the keys it doesn't own, so one patch can feed
247
+ // both stores safely (tag toggles → "autoTag", UI prefs → "ui").
246
248
  settings.setAutoTagConfig(patch);
249
+ settings.setUiConfig(patch);
247
250
  // Respond with the effective (settings-over-config merged) toggles
248
251
  // so the panel always sees booleans that actually gate runtime
249
252
  // behaviour (issue #31).
250
- sendJson(res, 200, { config: service.getTagConfig() });
253
+ sendJson(res, 200, { config: service.getAppConfig() });
251
254
  return;
252
255
  }
253
- sendJson(res, 200, { config: service.getTagConfig() });
256
+ sendJson(res, 200, { config: service.getAppConfig() });
254
257
  } catch {
255
258
  sendJson(res, 500, { error: "internal" });
256
259
  }
@@ -17,6 +17,11 @@ export const Config = z.object({
17
17
  summarizeModel: z.string().default(""),
18
18
  maxInjectedItems: z.natural().min(1).max(20).default(5),
19
19
  importanceThreshold: z.natural().min(1).max(5).default(3),
20
+ // 时间前缀注入(issue #34,opt-in,默认关):开启时,新对话开始时把当前
21
+ // 日期时间以 [当前时间: 2026-08-26 周二 19:30] 前缀注入一次,让模型感知
22
+ // "现在几点/周几"。只注入一次(对话开始时),同会话后续渲染不再重复;
23
+ // 关闭时行为与之前完全一致。
24
+ injectTimePrefix: z.boolean().default(false),
20
25
  autoDream: z.boolean().default(true),
21
26
  dreamThresholdCount: z.natural().min(1).max(1000).default(10),
22
27
  dreamThresholdChars: z.natural().min(100).max(100000).default(5000),
@@ -198,6 +203,16 @@ export const Config = z.object({
198
203
  // set false to disable the manual write path too.
199
204
  manualTagEnabled: z.boolean().default(true),
200
205
 
206
+ // --- sidebar trigger (issue #38) ---------------------------------------
207
+ // The memory library is reachable from two UI surfaces: a "记忆库" tab in the
208
+ // conversation header AND an entrance button at the sidebar footer (bottom
209
+ // left). Some sidebars get crowded there — dsh-cost-meter and friends also
210
+ // claim the footer slot — so the entrance button is now optional. Off just
211
+ // hides the button; the conversation tab stays, so no functionality is lost.
212
+ // The Web panel exposes this as a toggle (settings-over-config, see
213
+ // settings.getUiConfig); the value here is the plugin-config default.
214
+ showSidebarTrigger: z.boolean().default(true),
215
+
201
216
  // --- tag-weighted re-rank (v0.6.4) -------------------------------------
202
217
  // Opt-in: boost candidates whose tags overlap the query/session tags.
203
218
  tagBoostEnabled: z.boolean().default(false),
@@ -75,6 +75,32 @@ export function createInjector(ctx, service, settings, config) {
75
75
  const maxItems = config.maxInjectedItems ?? 5;
76
76
  const threshold = config.importanceThreshold ?? 3;
77
77
 
78
+ // Time-prefix injection (issue #34): opt-in, off by default. When enabled the
79
+ // current date/time is injected once per conversation — at the first prompt
80
+ // assembly of a new session — so the model can sense what time/day it is.
81
+ // A per-session latch means later assemblies in the same session never
82
+ // re-inject; a bare render ctx (no session id) falls back to once per
83
+ // injector lifetime.
84
+ const WEEKDAYS = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"];
85
+ let timePrefixSession = null;
86
+
87
+ function renderTimePrefix(ctx) {
88
+ if (config.injectTimePrefix !== true) return "";
89
+ const sessionId = ctx?.agent?.session?.id;
90
+ if (sessionId === undefined) {
91
+ if (timePrefixSession !== null) return "";
92
+ timePrefixSession = true;
93
+ } else {
94
+ if (sessionId === timePrefixSession) return "";
95
+ timePrefixSession = sessionId;
96
+ }
97
+ const now = new Date();
98
+ const pad = (n) => String(n).padStart(2, "0");
99
+ const date = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
100
+ const time = `${pad(now.getHours())}:${pad(now.getMinutes())}`;
101
+ return `[当前时间: ${date} ${WEEKDAYS[now.getDay()]} ${time}]`;
102
+ }
103
+
78
104
  // Bug6: bound the injected memory block. Each entry's content is truncated to
79
105
  // MAX_CONTENT chars (trailing `…`); the whole block gets a MAX_BLOCK budget
80
106
  // and an entry that would exceed it collapses to its title only, so a long
@@ -188,8 +214,11 @@ export function createInjector(ctx, service, settings, config) {
188
214
  // separate context) keeps the prompt assembly stable at two blocks.
189
215
  const hotText = renderHotContext(ctx);
190
216
  const body = render(candidates);
191
- if (!hotText) return body;
192
- return body ? `${hotText}\n\n${body}` : hotText;
217
+ let out = !hotText ? body : (body ? `${hotText}\n\n${body}` : hotText);
218
+ // Time prefix (issue #34): leads the injection block once per session.
219
+ const timePrefix = renderTimePrefix(ctx);
220
+ if (!timePrefix) return out;
221
+ return out ? `${timePrefix}\n\n${out}` : timePrefix;
193
222
  }
194
223
  }),
195
224
  ctx.systemPrompt.context({
@@ -105,6 +105,19 @@ export function createService({ store, mirror, config, onWrite, logger, settings
105
105
  };
106
106
  };
107
107
 
108
+ // App-wide effective config for the Web panel (/api/dsh-mneme/config): the
109
+ // tag toggles plus UI preferences (issue #38). Settings overrides plugin
110
+ // config when explicitly stored; unset keys fall back to the zod default.
111
+ const appConfig = () => {
112
+ const stored = settings?.getUiConfig?.() ?? {};
113
+ return {
114
+ ...tagConfig(),
115
+ showSidebarTrigger: typeof stored.showSidebarTrigger === "boolean"
116
+ ? stored.showSidebarTrigger
117
+ : (config.showSidebarTrigger ?? true)
118
+ };
119
+ };
120
+
108
121
  // Optional recall recorder, installed via setRecallRecorder after creation.
109
122
  // When searchMemories is called with recordRecall=true it receives the
110
123
  // actual merged recall scene (candidates + scores + source + threshold) so
@@ -1803,6 +1816,7 @@ export function createService({ store, mirror, config, onWrite, logger, settings
1803
1816
  // Effective tag toggle pair (settings-over-config merge) for the panel
1804
1817
  // (/api/dsh-mneme/config GET/PUT).
1805
1818
  getTagConfig: tagConfig,
1819
+ getAppConfig: appConfig,
1806
1820
  applyMemoryTags: (memoryId, tags) => store.setMemoryTags(memoryId, tags),
1807
1821
  saveAttr: (r) => store.saveAttr(r),
1808
1822
  createEntity: (r) => store.createEntity(r),