@modusensus/dsh-mneme 0.7.0 → 0.7.2

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 +415 -149
  2. package/{dsh-mneme/src → lib}/api.js +6 -3
  3. package/{dsh-mneme/lib → lib}/client.js +55 -8
  4. package/{dsh-mneme/src → lib}/config.js +5 -0
  5. package/{dsh-mneme/src → lib}/dream.js +7 -2
  6. package/{dsh-mneme/lib → lib}/index.js +7 -6
  7. package/{dsh-mneme/lib → lib}/inject.js +31 -2
  8. package/{dsh-mneme/src → lib}/service.js +37 -3
  9. package/{dsh-mneme/src → lib}/settings.js +15 -6
  10. package/{dsh-mneme/src → lib}/store.js +5 -0
  11. package/package.json +40 -18
  12. package/{dsh-mneme/lib → src}/api.js +6 -3
  13. package/{dsh-mneme/lib → src}/config.js +5 -0
  14. package/{dsh-mneme/lib → src}/dream.js +7 -2
  15. package/{dsh-mneme/src → src}/index.js +7 -6
  16. package/{dsh-mneme/src → src}/inject.js +31 -2
  17. package/{dsh-mneme/lib → src}/service.js +37 -3
  18. package/{dsh-mneme/lib → src}/settings.js +15 -6
  19. package/{dsh-mneme/lib → src}/store.js +5 -0
  20. package/{dsh-mneme/test → test}/api.test.js +10 -3
  21. package/{dsh-mneme/test → test}/client.test.js +37 -0
  22. package/{dsh-mneme/test → test}/config.test.js +7 -0
  23. package/{dsh-mneme/test → test}/inject.test.js +28 -0
  24. package/{dsh-mneme/test → test}/settings.test.js +6 -2
  25. package/{dsh-mneme/test → test}/tag.test.js +114 -0
  26. package/.github/workflows/test.yml +0 -32
  27. package/CHANGELOG.md +0 -89
  28. package/CONTRIBUTING.md +0 -245
  29. package/SECURITY.md +0 -544
  30. package/docs/devlog/2026-08-14-dsh-mneme-dev-log.md +0 -247
  31. package/docs/devlog/2026-08-15-dsh-mneme-audit-stress-dev-log.md +0 -145
  32. package/docs/devlog/2026-08-15-dsh-mneme-pipeline-dev-log.md +0 -56
  33. package/docs/devlog/2026-08-15-dsh-mneme-reflection-dev-log.md +0 -77
  34. package/docs/devlog/2026-08-15-dsh-mneme-review-fixes-dev-log.md +0 -64
  35. package/docs/devlog/2026-08-15-dsh-mneme-semantic-dev-log.md +0 -90
  36. package/dsh-mneme/CHANGELOG.md +0 -286
  37. package/dsh-mneme/LICENSE +0 -21
  38. package/dsh-mneme/README.md +0 -482
  39. package/dsh-mneme/docs/AGENT_MEMORY_RESEARCH.md +0 -183
  40. package/dsh-mneme/docs/ENTITIES.md +0 -245
  41. package/dsh-mneme/docs/LOCAL_MODEL.md +0 -141
  42. package/dsh-mneme/docs/MIGRATION.md +0 -127
  43. package/dsh-mneme/docs/SEMANTIC.md +0 -256
  44. package/dsh-mneme/docs/SLEEP.md +0 -163
  45. package/dsh-mneme/package-lock.json +0 -1936
  46. package/dsh-mneme/package.json +0 -80
  47. package/v0.7-plan-2026-08-21.md +0 -75
  48. package//346/250/252/345/271/205.png +0 -0
  49. /package/{dsh-mneme/cordis.patch.yml → cordis.patch.yml} +0 -0
  50. /package/{dsh-mneme/lib → lib}/commands.js +0 -0
  51. /package/{dsh-mneme/lib → lib}/dream/clustering.js +0 -0
  52. /package/{dsh-mneme/lib → lib}/dream/decisions.js +0 -0
  53. /package/{dsh-mneme/lib → lib}/dream/sleep.js +0 -0
  54. /package/{dsh-mneme/lib → lib}/dream/tag-extractor.js +0 -0
  55. /package/{dsh-mneme/lib → lib}/embedding.js +0 -0
  56. /package/{dsh-mneme/lib → lib}/entities/extractor.js +0 -0
  57. /package/{dsh-mneme/lib → lib}/heat.js +0 -0
  58. /package/{dsh-mneme/lib → lib}/hot-memory.js +0 -0
  59. /package/{dsh-mneme/lib → lib}/local-embedder.js +0 -0
  60. /package/{dsh-mneme/lib → lib}/mirror.js +0 -0
  61. /package/{dsh-mneme/lib → lib}/parser/tag.js +0 -0
  62. /package/{dsh-mneme/lib → lib}/parser/wiki-link.js +0 -0
  63. /package/{dsh-mneme/lib → lib}/quality-filter.js +0 -0
  64. /package/{dsh-mneme/lib → lib}/reranker.js +0 -0
  65. /package/{dsh-mneme/lib → lib}/search/adaptive.js +0 -0
  66. /package/{dsh-mneme/lib → lib}/search/bm25.js +0 -0
  67. /package/{dsh-mneme/lib → lib}/search/tag-boost.js +0 -0
  68. /package/{dsh-mneme/lib → lib}/summarize.js +0 -0
  69. /package/{dsh-mneme/lib → lib}/tools.js +0 -0
  70. /package/{dsh-mneme/lib → lib}/vector-index.js +0 -0
  71. /package/{dsh-mneme/scripts → scripts}/benchmark-embed.js +0 -0
  72. /package/{dsh-mneme/scripts → scripts}/benchmark-recall.js +0 -0
  73. /package/{dsh-mneme/scripts → scripts}/benchmark-rerank.js +0 -0
  74. /package/{dsh-mneme/scripts → scripts}/e2e-dsh.js +0 -0
  75. /package/{dsh-mneme/scripts → scripts}/stress-dsh.js +0 -0
  76. /package/{dsh-mneme/scripts → scripts}/sync-lib.js +0 -0
  77. /package/{dsh-mneme/src → src}/commands.js +0 -0
  78. /package/{dsh-mneme/src → src}/dream/clustering.js +0 -0
  79. /package/{dsh-mneme/src → src}/dream/decisions.js +0 -0
  80. /package/{dsh-mneme/src → src}/dream/sleep.js +0 -0
  81. /package/{dsh-mneme/src → src}/dream/tag-extractor.js +0 -0
  82. /package/{dsh-mneme/src → src}/embedding.js +0 -0
  83. /package/{dsh-mneme/src → src}/entities/extractor.js +0 -0
  84. /package/{dsh-mneme/src → src}/heat.js +0 -0
  85. /package/{dsh-mneme/src → src}/hot-memory.js +0 -0
  86. /package/{dsh-mneme/src → src}/local-embedder.js +0 -0
  87. /package/{dsh-mneme/src → src}/mirror.js +0 -0
  88. /package/{dsh-mneme/src → src}/parser/tag.js +0 -0
  89. /package/{dsh-mneme/src → src}/parser/wiki-link.js +0 -0
  90. /package/{dsh-mneme/src → src}/quality-filter.js +0 -0
  91. /package/{dsh-mneme/src → src}/reranker.js +0 -0
  92. /package/{dsh-mneme/src → src}/search/adaptive.js +0 -0
  93. /package/{dsh-mneme/src → src}/search/bm25.js +0 -0
  94. /package/{dsh-mneme/src → src}/search/tag-boost.js +0 -0
  95. /package/{dsh-mneme/src → src}/summarize.js +0 -0
  96. /package/{dsh-mneme/src → src}/tools.js +0 -0
  97. /package/{dsh-mneme/src → src}/vector-index.js +0 -0
  98. /package/{dsh-mneme/test → test}/audit.test.js +0 -0
  99. /package/{dsh-mneme/test → test}/benchmark.test.js +0 -0
  100. /package/{dsh-mneme/test → test}/boundary-v0625.test.js +0 -0
  101. /package/{dsh-mneme/test → test}/clustering.test.js +0 -0
  102. /package/{dsh-mneme/test → test}/commands.test.js +0 -0
  103. /package/{dsh-mneme/test → test}/conflict-freeze.test.js +0 -0
  104. /package/{dsh-mneme/test → test}/directory.test.js +0 -0
  105. /package/{dsh-mneme/test → test}/dream.test.js +0 -0
  106. /package/{dsh-mneme/test → test}/entities.test.js +0 -0
  107. /package/{dsh-mneme/test → test}/epistemic.test.js +0 -0
  108. /package/{dsh-mneme/test → test}/fnew-0112.test.js +0 -0
  109. /package/{dsh-mneme/test → test}/fnew-03.test.js +0 -0
  110. /package/{dsh-mneme/test → test}/graph-api.test.js +0 -0
  111. /package/{dsh-mneme/test → test}/heat.test.js +0 -0
  112. /package/{dsh-mneme/test → test}/helpers/dream-mock.js +0 -0
  113. /package/{dsh-mneme/test → test}/hot-memory.test.js +0 -0
  114. /package/{dsh-mneme/test → test}/llm-audit.test.js +0 -0
  115. /package/{dsh-mneme/test → test}/local-embedder.test.js +0 -0
  116. /package/{dsh-mneme/test → test}/mirror-dirty.test.js +0 -0
  117. /package/{dsh-mneme/test → test}/mirror-edit-digest.test.js +0 -0
  118. /package/{dsh-mneme/test → test}/mirror-generation.test.js +0 -0
  119. /package/{dsh-mneme/test → test}/mirror.test.js +0 -0
  120. /package/{dsh-mneme/test → test}/normalize-decisions.test.js +0 -0
  121. /package/{dsh-mneme/test → test}/peer-blockers.test.js +0 -0
  122. /package/{dsh-mneme/test → test}/policy-epoch.test.js +0 -0
  123. /package/{dsh-mneme/test → test}/provenance.test.js +0 -0
  124. /package/{dsh-mneme/test → test}/quality-filter.test.js +0 -0
  125. /package/{dsh-mneme/test → test}/reasoning-effort.test.js +0 -0
  126. /package/{dsh-mneme/test → test}/recall-evals.test.js +0 -0
  127. /package/{dsh-mneme/test → test}/recall-layer.test.js +0 -0
  128. /package/{dsh-mneme/test → test}/recall-runs.test.js +0 -0
  129. /package/{dsh-mneme/test → test}/receipt-chain.test.js +0 -0
  130. /package/{dsh-mneme/test → test}/reflection.test.js +0 -0
  131. /package/{dsh-mneme/test → test}/reranker.test.js +0 -0
  132. /package/{dsh-mneme/test → test}/search-fusion.test.js +0 -0
  133. /package/{dsh-mneme/test → test}/semantic.test.js +0 -0
  134. /package/{dsh-mneme/test → test}/service-search.test.js +0 -0
  135. /package/{dsh-mneme/test → test}/service.test.js +0 -0
  136. /package/{dsh-mneme/test → test}/sleep-heat.test.js +0 -0
  137. /package/{dsh-mneme/test → test}/sleep.test.js +0 -0
  138. /package/{dsh-mneme/test → test}/store.test.js +0 -0
  139. /package/{dsh-mneme/test → test}/stress.test.js +0 -0
  140. /package/{dsh-mneme/test → test}/summarize.test.js +0 -0
  141. /package/{dsh-mneme/test → test}/tag-boost.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
@@ -243,11 +243,14 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
243
243
  sendJson(res, 400, { error: "no valid config field provided" });
244
244
  return;
245
245
  }
246
- const config = settings.setAutoTagConfig(patch);
247
- sendJson(res, 200, { config });
246
+ settings.setAutoTagConfig(patch);
247
+ // Respond with the effective (settings-over-config merged) toggles
248
+ // so the panel always sees booleans that actually gate runtime
249
+ // behaviour (issue #31).
250
+ sendJson(res, 200, { config: service.getTagConfig() });
248
251
  return;
249
252
  }
250
- sendJson(res, 200, { config: settings.getAutoTagConfig() });
253
+ sendJson(res, 200, { config: service.getTagConfig() });
251
254
  } catch {
252
255
  sendJson(res, 500, { error: "internal" });
253
256
  }
@@ -122,6 +122,8 @@ window.__ModuleLoader__.load({
122
122
  "memory.directory.empty": "暂无记忆条目",
123
123
  "directory.editToggle": "编辑模式(显示删除)",
124
124
  "directory.deleteConfirm": "确定删除这条记忆吗?",
125
+ "directory.deleteConfirmShort": "确认?",
126
+ "directory.deleteError": "删除失败",
125
127
  "memory.card.open": "在主区记忆库中查看全文",
126
128
  "memory.time.now": "刚刚",
127
129
  "memory.time.seconds": "{n}秒前",
@@ -229,6 +231,8 @@ window.__ModuleLoader__.load({
229
231
  "memory.directory.empty": "No memories yet",
230
232
  "directory.editToggle": "Edit mode",
231
233
  "directory.deleteConfirm": "Delete this memory?",
234
+ "directory.deleteConfirmShort": "Confirm?",
235
+ "directory.deleteError": "Delete failed",
232
236
  "memory.card.open": "Open full text in the Memory tab",
233
237
  "memory.time.now": "just now",
234
238
  "memory.time.seconds": "{n}s ago",
@@ -432,6 +436,9 @@ window.__ModuleLoader__.load({
432
436
  ".mneme-dir-del{margin-left:auto;background:none;border:none;color:#e5484d;cursor:pointer;font-size:13px;line-height:1;padding:2px 4px}",
433
437
  ".mneme-dir-del:hover{filter:brightness(1.25)}",
434
438
  ".mneme-edit-on .mneme-dir-del{opacity:1}",
439
+ ".mneme-dir-del-confirm{font-weight:700;background:rgba(229,72,77,.12);border-radius:4px}",
440
+ ".mneme-dir-item.mneme-dir-error{background:rgba(229,72,77,.14)}",
441
+ ".mneme-dir-del-err{flex:none;font-size:12px;line-height:16px;color:#e5484d}",
435
442
  ".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
443
  ".mneme-dir-folder{margin-bottom:4px}",
437
444
  ".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}",
@@ -1184,6 +1191,9 @@ window.__ModuleLoader__.load({
1184
1191
  const [dir, setDir] = useState(null); // { groups: [...], untagged: [...] }
1185
1192
  const [status, setStatus] = useState("loading"); // loading | ready | error
1186
1193
  const [editMode, setEditMode] = useState(false); // manual tag/delete UI gate
1194
+ const [confirmingId, setConfirmingId] = useState(null); // row waiting for the 2nd confirm click
1195
+ const [delErrorId, setDelErrorId] = useState(null); // row whose delete just failed
1196
+ const confirmTimer = useRef(null);
1187
1197
 
1188
1198
  useEffect(() => {
1189
1199
  let cancelled = false;
@@ -1215,9 +1225,24 @@ window.__ModuleLoader__.load({
1215
1225
  }).then((res) => { if (res.ok) setEditMode(next); }).catch(() => {});
1216
1226
  };
1217
1227
 
1228
+ const resetConfirm = () => {
1229
+ clearTimeout(confirmTimer.current);
1230
+ setConfirmingId(null);
1231
+ };
1232
+
1218
1233
  const handleDelete = (memId, e) => {
1219
1234
  e?.stopPropagation?.();
1220
- if (typeof window === "undefined" || !window.confirm(t("directory.deleteConfirm"))) return;
1235
+ // 第一击进入「确认?」态,第二击才真正发 DELETE;宿主 web 环境里
1236
+ // window.confirm 不可靠(可能被拦截或直接返回 false),点击会无反应,
1237
+ // 所以改为面板内联两步确认,不依赖任何原生对话框。
1238
+ if (confirmingId !== memId) {
1239
+ setConfirmingId(memId);
1240
+ clearTimeout(confirmTimer.current);
1241
+ confirmTimer.current = setTimeout(() => setConfirmingId((cur) => (cur === memId ? null : cur)), 3000);
1242
+ return;
1243
+ }
1244
+ clearTimeout(confirmTimer.current);
1245
+ setConfirmingId(null);
1221
1246
  apiFetch("/api/dsh-mneme/memories", {
1222
1247
  method: "DELETE",
1223
1248
  headers: { "Content-Type": "application/json" },
@@ -1225,26 +1250,48 @@ window.__ModuleLoader__.load({
1225
1250
  })
1226
1251
  .then((res) => (res.ok ? res.json() : null))
1227
1252
  .then((d) => {
1228
- if (!d || d.deleted !== true) return;
1253
+ if (!d || d.deleted !== true) {
1254
+ // 服务端未确认删除也给出可见提示,不静默吞掉
1255
+ setDelErrorId(memId);
1256
+ setTimeout(() => setDelErrorId((cur) => (cur === memId ? null : cur)), 3500);
1257
+ return;
1258
+ }
1229
1259
  setDir((prev) => ({
1230
1260
  groups: prev.groups.map((g) => ({ ...g, memories: g.memories.filter((m) => m.id !== memId) })),
1231
1261
  untagged: prev.untagged.filter((m) => m.id !== memId)
1232
1262
  }));
1233
1263
  })
1234
- .catch(() => {});
1264
+ .catch(() => {
1265
+ // 网络失败同样给出可见提示
1266
+ setDelErrorId(memId);
1267
+ setTimeout(() => setDelErrorId((cur) => (cur === memId ? null : cur)), 3500);
1268
+ });
1235
1269
  };
1236
1270
 
1237
1271
  const renderFolder = (name, memories, extraKey, emptyLabel) => {
1238
1272
  const folderKey = extraKey || name;
1239
1273
  const open = !collapsed[folderKey];
1240
- const rows = memories.map((m) =>
1241
- h("div", { key: m.id, className: "mneme-dir-item" },
1274
+ const rows = memories.map((m) => {
1275
+ const confirming = confirmingId === m.id;
1276
+ const err = delErrorId === m.id;
1277
+ return h("div", {
1278
+ key: m.id,
1279
+ className: err ? "mneme-dir-item mneme-dir-error" : "mneme-dir-item",
1280
+ // 点击行内其它地方取消「确认?」态
1281
+ onClick: resetConfirm
1282
+ },
1242
1283
  h("span", { className: "mneme-tree-line" }),
1243
1284
  h("button", { type: "button", className: "mneme-dir-ititle", onClick: () => onJump(m) }, m.title || m.content?.slice(0, 40)),
1244
1285
  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
- );
1286
+ editMode && h("button", {
1287
+ type: "button",
1288
+ className: confirming ? "mneme-dir-del mneme-dir-del-confirm" : "mneme-dir-del",
1289
+ "aria-label": "delete",
1290
+ onClick: (e) => handleDelete(m.id, e)
1291
+ }, confirming ? t("directory.deleteConfirmShort") : "✕"),
1292
+ err && h("span", { className: "mneme-dir-del-err" }, t("directory.deleteError"))
1293
+ );
1294
+ });
1248
1295
  return h("div", {
1249
1296
  key: folderKey,
1250
1297
  className: "mneme-dir-folder",
@@ -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),
@@ -559,7 +559,7 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
559
559
  if (inFlight) await inFlight.catch(() => {});
560
560
  }
561
561
 
562
- async function runDream(ctx, service, config) {
562
+ async function runDream(ctx, service, config, settings) {
563
563
  const logger = ctx.logger;
564
564
  let memories = service.all().filter((m) => !m.archived && m.type !== "summary");
565
565
  if (memories.length === 0) return { ok: true, applied: 0, skipped: true, summary: false };
@@ -838,7 +838,12 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
838
838
  // and always fail-safe — a tag failure must never change the consolidation
839
839
  // outcome reported below (it is logged and counted, nothing more).
840
840
  let autoTagged = 0;
841
- if (config.autoTagEnabled === true) {
841
+ // Effective opt-in (issue #31): the Web panel's settings toggle
842
+ // (user_settings) overrides the zod plugin config when explicitly stored;
843
+ // unset → fall back to config.autoTagEnabled. settings is null-safe (absent
844
+ // in tests / direct calls) so behaviour is unchanged without it.
845
+ const autoTagEnabled = settings?.getAutoTagConfig?.().autoTagEnabled ?? config.autoTagEnabled;
846
+ if (autoTagEnabled === true) {
842
847
  try {
843
848
  const tagResult = await runAutoTag({ ctx, service, config, route });
844
849
  autoTagged = tagResult?.tagged ?? 0;
@@ -60,7 +60,12 @@ export const apply = (ctx, config) => {
60
60
  store.purgeRecallRunsOlderThan(recallRetentionDays);
61
61
  } catch { /* non-fatal */ }
62
62
  const mirror = createMirror(memoryDir);
63
- const service = createService({ store, mirror, config: cfg, logger: ctx.logger });
63
+ // User-configurable settings (profile, rules) and custom commands share the
64
+ // same SQLite file but live in dedicated tables, isolated from memories.
65
+ // Created before the service so the tag-config resolver (issue #31) can merge
66
+ // the Web panel's persisted toggles over the plugin config at runtime.
67
+ const settings = createSettings(store.db);
68
+ const service = createService({ store, mirror, config: cfg, logger: ctx.logger, settings });
64
69
 
65
70
  // F-NEW-03: if the mirror sync failed last run (persisted dirty state), retry
66
71
  // a safe re-render at boot so a stale mirror converges without needing a
@@ -85,10 +90,6 @@ export const apply = (ctx, config) => {
85
90
  } catch { /* non-fatal: recall recording is bookkeeping */ }
86
91
  });
87
92
 
88
- // User-configurable settings (profile, rules) and custom commands share the
89
- // same SQLite file but live in dedicated tables, isolated from memories.
90
- const settings = createSettings(store.db);
91
-
92
93
  // Semantic pipeline: a local/ollama embedder when configured, otherwise the
93
94
  // legacy OpenAI-compatible embedder (settings-driven). The vector index wraps
94
95
  // the store's embedding column and tracks the active model fingerprint. A
@@ -251,7 +252,7 @@ export const apply = (ctx, config) => {
251
252
  delayMs: cfg.dreamDelayMs,
252
253
  logger: ctx.logger,
253
254
  semantic: { embedder, vectorIndex },
254
- onRun: () => (dream ? dream.runDream(ctx, service, cfg) : Promise.resolve({ ok: true, skipped: true }))
255
+ onRun: () => (dream ? dream.runDream(ctx, service, cfg, settings) : Promise.resolve({ ok: true, skipped: true }))
255
256
  });
256
257
  service.setDreamHook(() => dream.maybeSchedule(service));
257
258
  }
@@ -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({
@@ -65,7 +65,7 @@ export function computeRetrievalMetrics(actualIds, expectedIds) {
65
65
  };
66
66
  }
67
67
 
68
- export function createService({ store, mirror, config, onWrite, logger }) {
68
+ export function createService({ store, mirror, config, onWrite, logger, settings }) {
69
69
  // Optional dream scheduler hook, installed via setDreamHook after creation
70
70
  // (the scheduler holds a reference back to the service, so it cannot be
71
71
  // passed in the constructor). Fired on the same write events as onWrite.
@@ -91,6 +91,20 @@ export function createService({ store, mirror, config, onWrite, logger }) {
91
91
  let vectorIndex = null;
92
92
  let reranker = null;
93
93
 
94
+ // Effective tag toggles (issue #31): the settings layer (Web panel switches,
95
+ // persisted in user_settings) overrides the plugin config when explicitly
96
+ // stored; unset keys fall back to the zod config default. This is the single
97
+ // runtime source of truth for setMemoryTags' manual gate and the Web panel's
98
+ // getTagConfig display — the panel toggle and the dream autoTag gate were
99
+ // previously writing/reading two disconnected stores.
100
+ const tagConfig = () => {
101
+ const stored = settings?.getAutoTagConfig?.() ?? {};
102
+ return {
103
+ autoTagEnabled: typeof stored.autoTagEnabled === "boolean" ? stored.autoTagEnabled : (config.autoTagEnabled ?? false),
104
+ manualTagEnabled: typeof stored.manualTagEnabled === "boolean" ? stored.manualTagEnabled : (config.manualTagEnabled ?? true)
105
+ };
106
+ };
107
+
94
108
  // Optional recall recorder, installed via setRecallRecorder after creation.
95
109
  // When searchMemories is called with recordRecall=true it receives the
96
110
  // actual merged recall scene (candidates + scores + source + threshold) so
@@ -933,6 +947,13 @@ export function createService({ store, mirror, config, onWrite, logger }) {
933
947
  });
934
948
  // Bug7: a degraded/archived result is applied on top of the merged row.
935
949
  const result = applyQualityDisposition(merged, quality, qf);
950
+ // Bridge tool-passed tags (plus any quality signal tags) into the
951
+ // entity_attrs tag store so directory / tag: recall / tagBoost see them
952
+ // (issue #31). Must run before afterSync so the mirror re-render reads
953
+ // the bridged row. Unconditional on the array (empty included): a merge
954
+ // that clears tags must also invalidate any stale entity_attrs row from
955
+ // an earlier save, or the directory keeps showing the old tag.
956
+ if (Array.isArray(result.tags)) store.setMemoryTags(result.id, result.tags);
936
957
  afterSync("write");
937
958
  notifyWrite();
938
959
  scheduleEmbed(result);
@@ -952,6 +973,11 @@ export function createService({ store, mirror, config, onWrite, logger }) {
952
973
  ...(quality ? { quality_score: quality.score } : {})
953
974
  });
954
975
  const result = applyQualityDisposition(created, quality, qf);
976
+ // Bridge tool-passed tags (plus any quality signal tags) into the
977
+ // entity_attrs tag store so directory / tag: recall / tagBoost see them
978
+ // (issue #31). Must run before afterSync so the mirror re-render reads
979
+ // the bridged row.
980
+ if (Array.isArray(result.tags) && result.tags.length) store.setMemoryTags(result.id, result.tags);
955
981
  afterSync("write");
956
982
  notifyWrite();
957
983
  scheduleEmbed(result);
@@ -1598,6 +1624,11 @@ export function createService({ store, mirror, config, onWrite, logger }) {
1598
1624
  update: (id, p, ctx = {}) => {
1599
1625
  const old = store.getById(id);
1600
1626
  const updated = store.update(id, p);
1627
+ // Bridge tag edits made through update (memory_update tool / panel) into
1628
+ // the entity_attrs tag store too (issue #31) — same convergence as
1629
+ // saveWithDedupe. An explicit empty array clears the live tag row, moving
1630
+ // the memory back to `untagged`.
1631
+ if (p?.tags !== undefined) store.setMemoryTags(updated.id, updated.tags);
1601
1632
  // Record a user correction when any meaningful field changed and the
1602
1633
  // reflection failure tracker is enabled. expected = what it became,
1603
1634
  // actual = what it was before; query (when provided) captures the
@@ -1757,7 +1788,7 @@ export function createService({ store, mirror, config, onWrite, logger }) {
1757
1788
  // tag pass (gated by autoTagEnabled, wrapped in a transaction by the
1758
1789
  // extractor so the mirror re-renders exactly once).
1759
1790
  setMemoryTags: (memoryId, tags) => {
1760
- if (config.manualTagEnabled === false) {
1791
+ if (tagConfig().manualTagEnabled === false) {
1761
1792
  return { ok: false, error: "manualTagEnabled is off" };
1762
1793
  }
1763
1794
  const stored = store.setMemoryTags(memoryId, tags);
@@ -1768,7 +1799,10 @@ export function createService({ store, mirror, config, onWrite, logger }) {
1768
1799
  getMemoryTags: (memoryId) => store.getMemoryTags(memoryId),
1769
1800
  // Read-only gate flag so the Web panel can hide tag editing when the
1770
1801
  // manual path is disabled (default: manual tagging is on).
1771
- manualTagEnabled: () => config.manualTagEnabled !== false,
1802
+ manualTagEnabled: () => tagConfig().manualTagEnabled !== false,
1803
+ // Effective tag toggle pair (settings-over-config merge) for the panel
1804
+ // (/api/dsh-mneme/config GET/PUT).
1805
+ getTagConfig: tagConfig,
1772
1806
  applyMemoryTags: (memoryId, tags) => store.setMemoryTags(memoryId, tags),
1773
1807
  saveAttr: (r) => store.saveAttr(r),
1774
1808
  createEntity: (r) => store.createEntity(r),
@@ -148,9 +148,13 @@ export function createSettings(db) {
148
148
  if (j && typeof j === "object") stored = j;
149
149
  } catch { /* fall through to defaults */ }
150
150
  }
151
+ // null = never explicitly stored → callers fall back to the plugin
152
+ // config default, so the settings layer can't override a toggle the
153
+ // user never touched (issue #31). This also unifies the manualTagEnabled
154
+ // default (settings no longer invents false where plugin config says true).
151
155
  return {
152
- autoTagEnabled: stored.autoTagEnabled === true,
153
- manualTagEnabled: stored.manualTagEnabled === true
156
+ autoTagEnabled: typeof stored.autoTagEnabled === "boolean" ? stored.autoTagEnabled : null,
157
+ manualTagEnabled: typeof stored.manualTagEnabled === "boolean" ? stored.manualTagEnabled : null
154
158
  };
155
159
  },
156
160
  setAutoTagConfig(partial) {
@@ -161,10 +165,15 @@ export function createSettings(db) {
161
165
  return j && typeof j === "object" ? j : {};
162
166
  } catch { return {}; }
163
167
  })();
164
- const cfg = {
165
- autoTagEnabled: partial.autoTagEnabled === undefined ? cur.autoTagEnabled === true : partial.autoTagEnabled === true,
166
- manualTagEnabled: partial.manualTagEnabled === undefined ? cur.manualTagEnabled === true : partial.manualTagEnabled === true
167
- };
168
+ const cfg = {};
169
+ // Only persist keys the caller explicitly set (or already stored). A
170
+ // partial update must never invent `false` for a key the user left
171
+ // alone — that would silently disable manual tagging on a toggle of
172
+ // autoTag alone (issue #31).
173
+ if (partial.autoTagEnabled !== undefined) cfg.autoTagEnabled = partial.autoTagEnabled === true;
174
+ else if (cur.autoTagEnabled !== undefined) cfg.autoTagEnabled = cur.autoTagEnabled === true;
175
+ if (partial.manualTagEnabled !== undefined) cfg.manualTagEnabled = partial.manualTagEnabled === true;
176
+ else if (cur.manualTagEnabled !== undefined) cfg.manualTagEnabled = cur.manualTagEnabled === true;
168
177
  setSetting("autoTag", JSON.stringify(cfg));
169
178
  return cfg;
170
179
  }
@@ -1743,6 +1743,11 @@ export function createStore(path) {
1743
1743
  VALUES (?, ?, 'tags', ?, ?, ?, NULL, 1.0, 'manual')`
1744
1744
  ).run(id, memoryId, JSON.stringify(arr), memoryId, now);
1745
1745
  }
1746
+ // Keep memories.tags (the display/search column) as a mirror of the
1747
+ // entity_attrs source of truth, so API rows and keyword search never
1748
+ // drift from the directory/tag store (issue #31).
1749
+ db.prepare("UPDATE memories SET tags = ?, updated_at = ? WHERE id = ?")
1750
+ .run(JSON.stringify(arr), now, memoryId);
1746
1751
  db.exec("RELEASE set_memory_tags");
1747
1752
  } catch (e) {
1748
1753
  db.exec("ROLLBACK TO set_memory_tags");
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@modusensus/dsh-mneme",
3
- "version": "0.7.0",
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.2",
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,31 @@
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": ["text", "lcov"]
57
79
  }
58
80
  }
@@ -243,11 +243,14 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
243
243
  sendJson(res, 400, { error: "no valid config field provided" });
244
244
  return;
245
245
  }
246
- const config = settings.setAutoTagConfig(patch);
247
- sendJson(res, 200, { config });
246
+ settings.setAutoTagConfig(patch);
247
+ // Respond with the effective (settings-over-config merged) toggles
248
+ // so the panel always sees booleans that actually gate runtime
249
+ // behaviour (issue #31).
250
+ sendJson(res, 200, { config: service.getTagConfig() });
248
251
  return;
249
252
  }
250
- sendJson(res, 200, { config: settings.getAutoTagConfig() });
253
+ sendJson(res, 200, { config: service.getTagConfig() });
251
254
  } catch {
252
255
  sendJson(res, 500, { error: "internal" });
253
256
  }
@@ -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),
@@ -559,7 +559,7 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
559
559
  if (inFlight) await inFlight.catch(() => {});
560
560
  }
561
561
 
562
- async function runDream(ctx, service, config) {
562
+ async function runDream(ctx, service, config, settings) {
563
563
  const logger = ctx.logger;
564
564
  let memories = service.all().filter((m) => !m.archived && m.type !== "summary");
565
565
  if (memories.length === 0) return { ok: true, applied: 0, skipped: true, summary: false };
@@ -838,7 +838,12 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
838
838
  // and always fail-safe — a tag failure must never change the consolidation
839
839
  // outcome reported below (it is logged and counted, nothing more).
840
840
  let autoTagged = 0;
841
- if (config.autoTagEnabled === true) {
841
+ // Effective opt-in (issue #31): the Web panel's settings toggle
842
+ // (user_settings) overrides the zod plugin config when explicitly stored;
843
+ // unset → fall back to config.autoTagEnabled. settings is null-safe (absent
844
+ // in tests / direct calls) so behaviour is unchanged without it.
845
+ const autoTagEnabled = settings?.getAutoTagConfig?.().autoTagEnabled ?? config.autoTagEnabled;
846
+ if (autoTagEnabled === true) {
842
847
  try {
843
848
  const tagResult = await runAutoTag({ ctx, service, config, route });
844
849
  autoTagged = tagResult?.tagged ?? 0;
@@ -60,7 +60,12 @@ export const apply = (ctx, config) => {
60
60
  store.purgeRecallRunsOlderThan(recallRetentionDays);
61
61
  } catch { /* non-fatal */ }
62
62
  const mirror = createMirror(memoryDir);
63
- const service = createService({ store, mirror, config: cfg, logger: ctx.logger });
63
+ // User-configurable settings (profile, rules) and custom commands share the
64
+ // same SQLite file but live in dedicated tables, isolated from memories.
65
+ // Created before the service so the tag-config resolver (issue #31) can merge
66
+ // the Web panel's persisted toggles over the plugin config at runtime.
67
+ const settings = createSettings(store.db);
68
+ const service = createService({ store, mirror, config: cfg, logger: ctx.logger, settings });
64
69
 
65
70
  // F-NEW-03: if the mirror sync failed last run (persisted dirty state), retry
66
71
  // a safe re-render at boot so a stale mirror converges without needing a
@@ -85,10 +90,6 @@ export const apply = (ctx, config) => {
85
90
  } catch { /* non-fatal: recall recording is bookkeeping */ }
86
91
  });
87
92
 
88
- // User-configurable settings (profile, rules) and custom commands share the
89
- // same SQLite file but live in dedicated tables, isolated from memories.
90
- const settings = createSettings(store.db);
91
-
92
93
  // Semantic pipeline: a local/ollama embedder when configured, otherwise the
93
94
  // legacy OpenAI-compatible embedder (settings-driven). The vector index wraps
94
95
  // the store's embedding column and tracks the active model fingerprint. A
@@ -251,7 +252,7 @@ export const apply = (ctx, config) => {
251
252
  delayMs: cfg.dreamDelayMs,
252
253
  logger: ctx.logger,
253
254
  semantic: { embedder, vectorIndex },
254
- onRun: () => (dream ? dream.runDream(ctx, service, cfg) : Promise.resolve({ ok: true, skipped: true }))
255
+ onRun: () => (dream ? dream.runDream(ctx, service, cfg, settings) : Promise.resolve({ ok: true, skipped: true }))
255
256
  });
256
257
  service.setDreamHook(() => dream.maybeSchedule(service));
257
258
  }
@@ -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({