@modusensus/dsh-mneme 0.7.9 → 0.7.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/README.en.md +368 -0
  2. package/README.md +298 -167
  3. package/{dsh-mneme/cordis.patch.yml → cordis.patch.yml} +1 -0
  4. package/{dsh-mneme/lib → lib}/api.js +58 -284
  5. package/lib/client.js +2086 -0
  6. package/{dsh-mneme/src → lib}/config.js +6 -100
  7. package/{dsh-mneme/lib → lib}/dream/decisions.js +94 -143
  8. package/{dsh-mneme/src → lib}/dream/sleep.js +12 -152
  9. package/{dsh-mneme/lib → lib}/dream.js +22 -168
  10. package/{dsh-mneme/src → lib}/hot-memory.js +0 -7
  11. package/{dsh-mneme/src → lib}/index.js +6 -34
  12. package/{dsh-mneme/lib → lib}/inject.js +11 -53
  13. package/{dsh-mneme/src → lib}/mirror.js +1 -12
  14. package/{dsh-mneme/lib → lib}/quality-filter.js +1 -3
  15. package/{dsh-mneme/lib → lib}/reranker.js +1 -4
  16. package/{dsh-mneme/lib → lib}/service.js +20 -410
  17. package/{dsh-mneme/lib → lib}/settings.js +0 -70
  18. package/{dsh-mneme/lib → lib}/store.js +35 -450
  19. package/{dsh-mneme/lib → lib}/summarize.js +10 -10
  20. package/lib/tools.js +274 -0
  21. package/package.json +41 -19
  22. package/{dsh-mneme/scripts → scripts}/e2e-dsh.js +3 -3
  23. package/{dsh-mneme/scripts → scripts}/sync-lib.js +5 -10
  24. package/{dsh-mneme/src → src}/api.js +58 -284
  25. package/{dsh-mneme/lib → src}/config.js +6 -100
  26. package/{dsh-mneme/src → src}/dream/decisions.js +94 -143
  27. package/{dsh-mneme/lib → src}/dream/sleep.js +12 -152
  28. package/{dsh-mneme/src → src}/dream.js +22 -168
  29. package/{dsh-mneme/lib → src}/hot-memory.js +0 -7
  30. package/{dsh-mneme/lib → src}/index.js +6 -34
  31. package/{dsh-mneme/src → src}/inject.js +11 -53
  32. package/{dsh-mneme/lib → src}/mirror.js +1 -12
  33. package/{dsh-mneme/src → src}/quality-filter.js +1 -3
  34. package/{dsh-mneme/src → src}/reranker.js +1 -4
  35. package/{dsh-mneme/src → src}/service.js +20 -410
  36. package/{dsh-mneme/src → src}/settings.js +0 -70
  37. package/{dsh-mneme/src → src}/store.js +35 -450
  38. package/{dsh-mneme/src → src}/summarize.js +10 -10
  39. package/src/tools.js +274 -0
  40. package/{dsh-mneme/test → test}/api.test.js +1 -156
  41. package/{dsh-mneme/test → test}/client.test.js +22 -207
  42. package/{dsh-mneme/test → test}/config.test.js +0 -21
  43. package/{dsh-mneme/test → test}/dream.test.js +1 -160
  44. package/{dsh-mneme/test → test}/graph-api.test.js +0 -34
  45. package/{dsh-mneme/test → test}/hot-memory.test.js +0 -29
  46. package/test/inject.test.js +120 -0
  47. package/{dsh-mneme/test → test}/llm-audit.test.js +4 -4
  48. package/{dsh-mneme/test → test}/reasoning-effort.test.js +0 -27
  49. package/{dsh-mneme/test → test}/recall-layer.test.js +5 -26
  50. package/{dsh-mneme/test → test}/reranker.test.js +0 -43
  51. package/{dsh-mneme/test → test}/service-search.test.js +0 -25
  52. package/{dsh-mneme/test → test}/service.test.js +0 -106
  53. package/{dsh-mneme/test → test}/settings.test.js +0 -43
  54. package/{dsh-mneme/test → test}/sleep.test.js +4 -171
  55. package/{dsh-mneme/test → test}/store.test.js +0 -76
  56. package/{dsh-mneme/test → test}/summarize.test.js +16 -15
  57. package/test/tools.test.js +265 -0
  58. package/.github/workflows/publish.yml +0 -58
  59. package/.github/workflows/test.yml +0 -32
  60. package/CHANGELOG.md +0 -91
  61. package/CONTRIBUTING.md +0 -291
  62. package/SECURITY.md +0 -718
  63. package/docs/devlog/2026-08-14-dsh-mneme-dev-log.md +0 -247
  64. package/docs/devlog/2026-08-15-dsh-mneme-audit-stress-dev-log.md +0 -145
  65. package/docs/devlog/2026-08-15-dsh-mneme-pipeline-dev-log.md +0 -56
  66. package/docs/devlog/2026-08-15-dsh-mneme-reflection-dev-log.md +0 -77
  67. package/docs/devlog/2026-08-15-dsh-mneme-review-fixes-dev-log.md +0 -64
  68. package/docs/devlog/2026-08-15-dsh-mneme-semantic-dev-log.md +0 -90
  69. package/dsh-mneme/CHANGELOG.md +0 -406
  70. package/dsh-mneme/LICENSE +0 -21
  71. package/dsh-mneme/README.md +0 -498
  72. package/dsh-mneme/docs/AGENT_MEMORY_RESEARCH.md +0 -183
  73. package/dsh-mneme/docs/ENTITIES.md +0 -245
  74. package/dsh-mneme/docs/LOCAL_MODEL.md +0 -141
  75. package/dsh-mneme/docs/MIGRATION.md +0 -127
  76. package/dsh-mneme/docs/SEMANTIC.md +0 -256
  77. package/dsh-mneme/docs/SLEEP.md +0 -163
  78. package/dsh-mneme/lib/client.js +0 -2050
  79. package/dsh-mneme/lib/dream/tag-extractor.js +0 -156
  80. package/dsh-mneme/lib/heat.js +0 -136
  81. package/dsh-mneme/lib/parser/tag.js +0 -59
  82. package/dsh-mneme/lib/parser/wiki-link.js +0 -38
  83. package/dsh-mneme/lib/search/tag-boost.js +0 -61
  84. package/dsh-mneme/lib/tools.js +0 -458
  85. package/dsh-mneme/package-lock.json +0 -1936
  86. package/dsh-mneme/package.json +0 -83
  87. package/dsh-mneme/scripts/check-sync.js +0 -42
  88. package/dsh-mneme/src/client.js +0 -2050
  89. package/dsh-mneme/src/dream/tag-extractor.js +0 -156
  90. package/dsh-mneme/src/heat.js +0 -136
  91. package/dsh-mneme/src/parser/tag.js +0 -59
  92. package/dsh-mneme/src/parser/wiki-link.js +0 -38
  93. package/dsh-mneme/src/search/tag-boost.js +0 -61
  94. package/dsh-mneme/src/tools.js +0 -458
  95. package/dsh-mneme/test/boundary-v0625.test.js +0 -82
  96. package/dsh-mneme/test/directory.test.js +0 -134
  97. package/dsh-mneme/test/heat.test.js +0 -148
  98. package/dsh-mneme/test/inject.test.js +0 -206
  99. package/dsh-mneme/test/layered-types-stats.test.js +0 -144
  100. package/dsh-mneme/test/lib-smoke.test.js +0 -109
  101. package/dsh-mneme/test/normalize-decisions.test.js +0 -120
  102. package/dsh-mneme/test/provenance.test.js +0 -103
  103. package/dsh-mneme/test/recall-runs.test.js +0 -93
  104. package/dsh-mneme/test/sleep-heat.test.js +0 -112
  105. package/dsh-mneme/test/tag-boost.test.js +0 -125
  106. package/dsh-mneme/test/tag.test.js +0 -426
  107. package/dsh-mneme/test/tools.test.js +0 -674
  108. package/dsh-mneme/test/updated-at-semantics.test.js +0 -113
  109. package/dsh-mneme/test/wiki-link.test.js +0 -332
  110. package//346/250/252/345/271/205.png +0 -0
  111. /package/{dsh-mneme/lib → lib}/commands.js +0 -0
  112. /package/{dsh-mneme/lib → lib}/dream/clustering.js +0 -0
  113. /package/{dsh-mneme/lib → lib}/embedding.js +0 -0
  114. /package/{dsh-mneme/lib → lib}/entities/extractor.js +0 -0
  115. /package/{dsh-mneme/lib → lib}/local-embedder.js +0 -0
  116. /package/{dsh-mneme/lib → lib}/search/adaptive.js +0 -0
  117. /package/{dsh-mneme/lib → lib}/search/bm25.js +0 -0
  118. /package/{dsh-mneme/lib → lib}/vector-index.js +0 -0
  119. /package/{dsh-mneme/scripts → scripts}/benchmark-embed.js +0 -0
  120. /package/{dsh-mneme/scripts → scripts}/benchmark-recall.js +0 -0
  121. /package/{dsh-mneme/scripts → scripts}/benchmark-rerank.js +0 -0
  122. /package/{dsh-mneme/scripts → scripts}/stress-dsh.js +0 -0
  123. /package/{dsh-mneme/src → src}/commands.js +0 -0
  124. /package/{dsh-mneme/src → src}/dream/clustering.js +0 -0
  125. /package/{dsh-mneme/src → src}/embedding.js +0 -0
  126. /package/{dsh-mneme/src → src}/entities/extractor.js +0 -0
  127. /package/{dsh-mneme/src → src}/local-embedder.js +0 -0
  128. /package/{dsh-mneme/src → src}/search/adaptive.js +0 -0
  129. /package/{dsh-mneme/src → src}/search/bm25.js +0 -0
  130. /package/{dsh-mneme/src → src}/vector-index.js +0 -0
  131. /package/{dsh-mneme/test → test}/audit.test.js +0 -0
  132. /package/{dsh-mneme/test → test}/benchmark.test.js +0 -0
  133. /package/{dsh-mneme/test → test}/clustering.test.js +0 -0
  134. /package/{dsh-mneme/test → test}/commands.test.js +0 -0
  135. /package/{dsh-mneme/test → test}/conflict-freeze.test.js +0 -0
  136. /package/{dsh-mneme/test → test}/entities.test.js +0 -0
  137. /package/{dsh-mneme/test → test}/epistemic.test.js +0 -0
  138. /package/{dsh-mneme/test → test}/fnew-0112.test.js +0 -0
  139. /package/{dsh-mneme/test → test}/fnew-03.test.js +0 -0
  140. /package/{dsh-mneme/test → test}/helpers/dream-mock.js +0 -0
  141. /package/{dsh-mneme/test → test}/local-embedder.test.js +0 -0
  142. /package/{dsh-mneme/test → test}/mirror-dirty.test.js +0 -0
  143. /package/{dsh-mneme/test → test}/mirror-edit-digest.test.js +0 -0
  144. /package/{dsh-mneme/test → test}/mirror-generation.test.js +0 -0
  145. /package/{dsh-mneme/test → test}/mirror.test.js +0 -0
  146. /package/{dsh-mneme/test → test}/peer-blockers.test.js +0 -0
  147. /package/{dsh-mneme/test → test}/policy-epoch.test.js +0 -0
  148. /package/{dsh-mneme/test → test}/quality-filter.test.js +0 -0
  149. /package/{dsh-mneme/test → test}/recall-evals.test.js +0 -0
  150. /package/{dsh-mneme/test → test}/receipt-chain.test.js +0 -0
  151. /package/{dsh-mneme/test → test}/reflection.test.js +0 -0
  152. /package/{dsh-mneme/test → test}/search-fusion.test.js +0 -0
  153. /package/{dsh-mneme/test → test}/semantic.test.js +0 -0
  154. /package/{dsh-mneme/test → test}/stress.test.js +0 -0
  155. /package/{dsh-mneme/test → test}/vector-index.test.js +0 -0
@@ -88,23 +88,20 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
88
88
  const type = url.searchParams.get("type") ?? undefined;
89
89
  const limit = Number(url.searchParams.get("limit") ?? 50);
90
90
  const offset = Number(url.searchParams.get("offset") ?? 0);
91
- const items = service.toApiList(service.list({ type, limit, offset }));
92
- sendJson(res, 200, { items, total: service.count(type) });
93
- } catch {
94
- sendJson(res, 500, { error: "internal" });
95
- }
96
- }
97
- });
98
-
99
- register({
100
- kind: "exact",
101
- path: "/api/dsh-mneme/stats",
102
- handler(req, res) {
103
- try {
104
- const url = new URL(req.url, "http://localhost");
105
- const raw = url.searchParams.get("days");
106
- const days = Math.min(30, Math.max(1, parseInt(raw ?? "7", 10) || 7));
107
- sendJson(res, 200, service.stats({ days }));
91
+ // order=chrono: pure newest-first for paged browsing; default keeps
92
+ // the importance-ranked order other callers rely on.
93
+ const order = url.searchParams.get("order") ?? undefined;
94
+ // minImportance: numeric lower bound on importance (absent/NaN → no
95
+ // floor); source: exact match on the source column (empty → no filter).
96
+ const minRaw = url.searchParams.get("minImportance");
97
+ const minImportance = minRaw !== null && minRaw !== "" && !Number.isNaN(Number(minRaw))
98
+ ? Number(minRaw)
99
+ : undefined;
100
+ const source = url.searchParams.get("source") || undefined;
101
+ const items = service.toApiList(service.list({ type, limit, offset, order, minImportance, source }));
102
+ // Total honors the same filters as the rows, or the pager's
103
+ // has-more math breaks whenever minImportance/source is active.
104
+ sendJson(res, 200, { items, total: service.count(type, { minImportance, source }) });
108
105
  } catch {
109
106
  sendJson(res, 500, { error: "internal" });
110
107
  }
@@ -175,6 +172,40 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
175
172
  }
176
173
  });
177
174
 
175
+ // --- delete one memory by id ---
176
+ // Mutation route: apiToken-gated like the profile/rules writes above. POST
177
+ // body is JSON { id }. store.remove deletes silently, so existence is checked
178
+ // up front to give clients a distinguishable 404 instead of a fake success.
179
+ register({
180
+ kind: "exact",
181
+ path: "/api/dsh-mneme/delete",
182
+ handler(req, res) {
183
+ try {
184
+ if (req.method !== "POST") {
185
+ sendJson(res, 404, { error: "not-found" });
186
+ return;
187
+ }
188
+ if (!requireAuth(req, res, apiToken)) return;
189
+ return readBody(req).then((text) => {
190
+ const body = parseBody(text);
191
+ const id = typeof body.id === "string" ? body.id.trim() : "";
192
+ if (!id) {
193
+ sendJson(res, 400, { error: "missing-id" });
194
+ return;
195
+ }
196
+ if (!service.getById(id)) {
197
+ sendJson(res, 404, { error: "not-found" });
198
+ return;
199
+ }
200
+ service.remove(id);
201
+ sendJson(res, 200, { ok: true });
202
+ });
203
+ } catch {
204
+ sendJson(res, 500, { error: "internal" });
205
+ }
206
+ }
207
+ });
208
+
178
209
  // --- rules ---
179
210
  register({
180
211
  kind: "exact",
@@ -233,102 +264,6 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
233
264
  }
234
265
  });
235
266
 
236
- // --- app config read/write (partial: pass only the fields to change) ---
237
- register({
238
- kind: "exact",
239
- path: "/api/dsh-mneme/config",
240
- handler: async (req, res) => {
241
- try {
242
- if (!requireAuth(req, res, apiToken)) return;
243
- if (req.method === "PUT" || req.method === "POST") {
244
- const body = parseBody(await readBody(req));
245
- const patch = {};
246
- const setIfBoolean = (key) => {
247
- if (Object.prototype.hasOwnProperty.call(body, key)) {
248
- if (typeof body[key] !== "boolean") {
249
- sendJson(res, 400, { error: `${key} must be a boolean` });
250
- return false;
251
- }
252
- patch[key] = body[key];
253
- }
254
- return true;
255
- };
256
- if (!setIfBoolean("autoTagEnabled") || !setIfBoolean("manualTagEnabled") || !setIfBoolean("showSidebarTrigger")) return;
257
- if (Object.keys(patch).length === 0) {
258
- sendJson(res, 400, { error: "no valid config field provided" });
259
- return;
260
- }
261
- // Each setter ignores the keys it doesn't own, so one patch can feed
262
- // both stores safely (tag toggles → "autoTag", UI prefs → "ui").
263
- settings.setAutoTagConfig(patch);
264
- settings.setUiConfig(patch);
265
- // Respond with the effective (settings-over-config merged) toggles
266
- // so the panel always sees booleans that actually gate runtime
267
- // behaviour (issue #31).
268
- sendJson(res, 200, { config: service.getAppConfig() });
269
- return;
270
- }
271
- sendJson(res, 200, { config: service.getAppConfig() });
272
- } catch {
273
- sendJson(res, 500, { error: "internal" });
274
- }
275
- }
276
- });
277
-
278
- // --- delete a memory by exact id or best query match (mirrors memory_delete tool) ---
279
- register({
280
- kind: "exact",
281
- path: "/api/dsh-mneme/memories",
282
- handler: async (req, res) => {
283
- try {
284
- if (!requireAuth(req, res, apiToken)) return;
285
- if (req.method !== "DELETE") {
286
- sendJson(res, 405, { error: "method not allowed" });
287
- return;
288
- }
289
- const body = parseBody(await readBody(req));
290
- const hasId = Object.prototype.hasOwnProperty.call(body, "id");
291
- const hasQuery = Object.prototype.hasOwnProperty.call(body, "query");
292
- if ((hasId && hasQuery) || (!hasId && !hasQuery)) {
293
- sendJson(res, 400, { error: "provide exactly one of id or query" });
294
- return;
295
- }
296
- if (hasId) {
297
- if (typeof body.id !== "string" || !body.id.trim()) {
298
- sendJson(res, 400, { error: "invalid id" });
299
- return;
300
- }
301
- const mem = service.getById(body.id);
302
- if (!mem) {
303
- sendJson(res, 200, { deleted: false });
304
- return;
305
- }
306
- service.remove(body.id);
307
- sendJson(res, 200, { deleted: true, id: body.id });
308
- return;
309
- }
310
- if (typeof body.query !== "string" || !body.query.trim()) {
311
- sendJson(res, 400, { error: "invalid query" });
312
- return;
313
- }
314
- const results = await service.searchMemories(body.query, { mode: "auto", topK: 1, useRerank: true });
315
- if (!Array.isArray(results) || results.length === 0) {
316
- sendJson(res, 200, { deleted: false });
317
- return;
318
- }
319
- const mem = results[0];
320
- if (!mem || !mem.id) {
321
- sendJson(res, 200, { deleted: false });
322
- return;
323
- }
324
- service.remove(mem.id);
325
- sendJson(res, 200, { deleted: true, id: mem.id });
326
- } catch {
327
- sendJson(res, 500, { error: "internal" });
328
- }
329
- }
330
- });
331
-
332
267
  // --- vector re-index (backfill embeddings for rows missing them) ---
333
268
  register({
334
269
  kind: "exact",
@@ -479,10 +414,7 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
479
414
  name: n.name,
480
415
  type: n.type ?? null,
481
416
  mention_count: n.mention_count ?? 1,
482
- distance: n.distance,
483
- // v0.7.0 实体热投影:实体热 = 关联记忆 heat 聚合(max),前端据此
484
- // 缩放节点大小/明暗。heatEnabled=false 时 entityHeat 返回 null。
485
- heat: service.entityHeat?.(n.id) ?? null
417
+ distance: n.distance
486
418
  })),
487
419
  edges: [...edgeMap.values()].map((e) => ({
488
420
  id: e.id,
@@ -536,101 +468,19 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
536
468
  }
537
469
  });
538
470
 
539
- // --- wiki-link back links (v0.6.1) --------------------------------------
540
- // Read-only like the graph endpoints, so it stays open when apiToken is set.
541
- // GET /api/dsh-mneme/wikilinks/backlinks?id=<memoryId> memories whose
542
- // content carries a [[wiki-link]] resolving to the given memory.
471
+ // --- entity directory (entity gene v0.3.0): read-only list for the UI ---
472
+ // Flat entity list (newest-seen first). Attributes and relations stay on
473
+ // their own per-entity endpoints, the directory only carries the columns
474
+ // the rail needs: name, type, mention_count, first/last seen.
543
475
  register({
544
476
  kind: "exact",
545
- path: "/api/dsh-mneme/wikilinks/backlinks",
477
+ path: "/api/dsh-mneme/entities",
546
478
  handler(req, res) {
547
479
  try {
548
480
  const url = new URL(req.url, "http://localhost");
549
- const id = (url.searchParams.get("id") ?? "").trim();
550
- if (!id) {
551
- sendJson(res, 400, { error: "missing-id" });
552
- return;
553
- }
554
- const memory = service.getById?.(id) ?? null;
555
- const backlinks = (service.getBacklinks?.(id) ?? []).map(({ source, relation }) => ({
556
- id: source.id,
557
- title: source.title,
558
- type: source.type,
559
- created_at: relation.created_at
560
- }));
561
- sendJson(res, 200, {
562
- memoryId: id,
563
- memory: memory ? { id: memory.id, title: memory.title, type: memory.type } : null,
564
- backlinks
565
- });
566
- } catch {
567
- sendJson(res, 500, { error: "internal" });
568
- }
569
- }
570
- });
571
-
572
- // --- wiki-link forward links (v0.6.1) -----------------------------------
573
- // GET /api/dsh-mneme/wikilinks/forward?id=<memoryId> → memories the given
574
- // memory explicitly links to. Unresolved target titles surface with id:null.
575
- register({
576
- kind: "exact",
577
- path: "/api/dsh-mneme/wikilinks/forward",
578
- handler(req, res) {
579
- try {
580
- const url = new URL(req.url, "http://localhost");
581
- const id = (url.searchParams.get("id") ?? "").trim();
582
- if (!id) {
583
- sendJson(res, 400, { error: "missing-id" });
584
- return;
585
- }
586
- const memory = service.getById?.(id) ?? null;
587
- const links = (service.getForwardLinks?.(id) ?? []).map(({ target, relation }) => ({
588
- id: target?.id ?? null,
589
- title: target?.title ?? relation.to_entity,
590
- type: target?.type ?? null,
591
- created_at: relation.created_at
592
- }));
593
- sendJson(res, 200, {
594
- memoryId: id,
595
- memory: memory ? { id: memory.id, title: memory.title, type: memory.type } : null,
596
- links
597
- });
598
- } catch {
599
- sendJson(res, 500, { error: "internal" });
600
- }
601
- }
602
- });
603
-
604
- // --- wiki-link resolve (v0.6.1) -----------------------------------------
605
- // GET /api/dsh-mneme/wikilinks/resolve?title=<title> → case-insensitive exact
606
- // title match against the memories table (the resolution used when writing
607
- // links_to relations). 404 when no memory matches.
608
- register({
609
- kind: "exact",
610
- path: "/api/dsh-mneme/wikilinks/resolve",
611
- handler(req, res) {
612
- try {
613
- const url = new URL(req.url, "http://localhost");
614
- const title = (url.searchParams.get("title") ?? "").trim();
615
- if (!title) {
616
- sendJson(res, 400, { error: "missing-title" });
617
- return;
618
- }
619
- const memory = service.resolveWikiLink?.(title) ?? null;
620
- if (!memory) {
621
- sendJson(res, 404, { error: "memory-not-found", title });
622
- return;
623
- }
624
- // Note: no `source` field — it may carry file paths/internal host info
625
- // and this endpoint is read-only without auth when apiToken is set.
626
- sendJson(res, 200, {
627
- title,
628
- memory: {
629
- id: memory.id,
630
- title: memory.title,
631
- type: memory.type
632
- }
633
- });
481
+ const limit = Math.min(1000, Math.max(1, Number(url.searchParams.get("limit") ?? 500) || 500));
482
+ const entities = service.listEntities?.({ limit }) ?? [];
483
+ sendJson(res, 200, { entities, total: entities.length });
634
484
  } catch {
635
485
  sendJson(res, 500, { error: "internal" });
636
486
  }
@@ -722,84 +572,8 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
722
572
  }
723
573
  });
724
574
 
725
- // --- memory tags (v0.6.2) ------------------------------------------------
726
- // GET /api/dsh-mneme/memory/tags?id=<memoryId> → live entity_attrs-backed
727
- // tag set for one memory plus the manualTagEnabled gate (so the panel
728
- // can hide tag editing when the manual path is off). Read-only, stays
729
- // open when apiToken is set (like list/search/semantic).
730
- // POST /api/dsh-mneme/memory/tags { id, tags } → overwrite the live tag set
731
- // via service.setMemoryTags (manualTagEnabled gate); 409 when the gate
732
- // is closed. Auth-gated like the other write endpoints.
733
- register({
734
- kind: "exact",
735
- path: "/api/dsh-mneme/memory/tags",
736
- handler(req, res) {
737
- try {
738
- if (req.method === "POST" || req.method === "PUT") {
739
- if (!requireAuth(req, res, apiToken)) return;
740
- return readBody(req).then((text) => {
741
- const body = parseBody(text);
742
- const id = typeof body.id === "string" ? body.id.trim() : "";
743
- if (!id) {
744
- sendJson(res, 400, { error: "missing-id" });
745
- return;
746
- }
747
- const memory = service.getById?.(id) ?? null;
748
- if (!memory) {
749
- sendJson(res, 404, { error: "memory-not-found" });
750
- return;
751
- }
752
- const result = service.setMemoryTags(id, Array.isArray(body.tags) ? body.tags : []);
753
- if (result?.ok === false) {
754
- sendJson(res, 409, { error: result.error || "tags-disabled" });
755
- return;
756
- }
757
- sendJson(res, 200, { ok: true, memoryId: id, tags: result?.tags ?? [] });
758
- });
759
- }
760
- const url = new URL(req.url, "http://localhost");
761
- const id = (url.searchParams.get("id") ?? "").trim();
762
- if (!id) {
763
- sendJson(res, 400, { error: "missing-id" });
764
- return;
765
- }
766
- const memory = service.getById?.(id) ?? null;
767
- if (!memory) {
768
- sendJson(res, 404, { error: "memory-not-found" });
769
- return;
770
- }
771
- const tags = service.getMemoryTags?.(id) ?? [];
772
- sendJson(res, 200, {
773
- memoryId: id,
774
- tags: Array.isArray(tags) ? tags : [],
775
- manualTagEnabled: service.manualTagEnabled?.() ?? true
776
- });
777
- } catch {
778
- sendJson(res, 500, { error: "internal" });
779
- }
780
- }
781
- });
782
-
783
- // --- directory view (v0.6.3) ---------------------------------------------
784
- // GET /api/dsh-mneme/directory → memories grouped by tag as
785
- // { groups: [{ tag, memories: [...] }], untagged: [...] }. Live-only
786
- // (forgotten/archived/session-disposed excluded), groups tag-sorted, members
787
- // importance+updated DESC. Read-only, stays open when apiToken is set.
788
- register({
789
- kind: "exact",
790
- path: "/api/dsh-mneme/directory",
791
- handler(req, res) {
792
- try {
793
- const dir = service.getDirectory?.() ?? { groups: [], untagged: [] };
794
- sendJson(res, 200, dir);
795
- } catch {
796
- sendJson(res, 500, { error: "internal" });
797
- }
798
- }
799
- });
800
-
801
575
  return {
802
- routes: 19,
576
+ routes: 15,
803
577
  dispose: () => {
804
578
  for (const dispose of disposers) dispose();
805
579
  }
@@ -1,15 +1,9 @@
1
1
  import z from "@deepseek-ai/schemastery";
2
- import { TYPE_DECAY_DEFAULTS } from "./heat.js";
3
2
 
4
3
  export const Config = z.object({
5
4
  memoryDir: z.string().default("~/.dsh/memory"),
6
5
  autoInject: z.boolean().default(true),
7
6
  autoSummarize: z.boolean().default(true),
8
- // Session lifecycle (v0.6.0): when enabled, deleting/disposing a session also
9
- // archives every memory that was born in it (treating the session as a save
10
- // point — entries stay recoverable via memory_archive/restoreBySession).
11
- // Default OFF: legacy behavior, a disposed session leaves its memories active.
12
- sessionLifecycleEnabled: z.boolean().default(false),
13
7
  // Optional model override for summarization. When both are non-empty, they
14
8
  // take priority over the session's current model. Empty = use the session's
15
9
  // active provider/model (same as before).
@@ -17,18 +11,6 @@ export const Config = z.object({
17
11
  summarizeModel: z.string().default(""),
18
12
  maxInjectedItems: z.natural().min(1).max(20).default(5),
19
13
  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),
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),
32
14
  autoDream: z.boolean().default(true),
33
15
  dreamThresholdCount: z.natural().min(1).max(1000).default(10),
34
16
  dreamThresholdChars: z.natural().min(100).max(100000).default(5000),
@@ -37,13 +19,11 @@ export const Config = z.object({
37
19
  dreamModel: z.string(),
38
20
  dreamMaxTokens: z.natural().min(256).max(131072).default(8192),
39
21
  // Pass-through reasoning effort for dream's LLM calls. 'none' (default)
40
- // omits the field so the provider's own default applies; 'off' explicitly
41
- // disables thinking REQUIRED for thinking-type models (deepseek-v4-flash
42
- // etc.) that would otherwise drain the whole token budget into reasoning and
43
- // return an empty body ("no json array in llm output"); low/medium/high are
44
- // forwarded verbatim to cap reasoning spend.
22
+ // omits the field so the provider's own default applies; low/medium/high
23
+ // are forwarded verbatim. Useful to cap reasoning spend on thinking-type
24
+ // models that would otherwise drain the whole token budget and return an
25
+ // empty body ("no json array in llm output").
45
26
  dreamReasoningEffort: z.union([
46
- z.const("off"),
47
27
  z.const("low"),
48
28
  z.const("medium"),
49
29
  z.const("high"),
@@ -62,16 +42,6 @@ export const Config = z.object({
62
42
  // → 整单拒绝,防止残缺输出被隐式 keep 洗白成 ok 后再被真实 apply。0-1,
63
43
  // 默认 0.5(至少显式覆盖一半 snapshot)。
64
44
  dreamMinExplicitCoverage: z.number().min(0).max(1).default(0.5),
65
- // 跳过非法决策(Issue #26 P0,默认开):跨类型 merge 等"单条非法"决策不再
66
- // 让整批校验失败 → 跳过该决策、应用合法子集,run 记为 degraded(applied>0)。
67
- // 关闭后恢复旧的"任意非法即整单拒绝"(applied=0)。防洗白语义不受影响——
68
- // 显式覆盖率不足/update 超量等全局错误仍整单拒绝。
69
- dreamSkipInvalid: z.boolean().default(true),
70
- // 允许跨类型合并(Issue #26 P1,默认关):类型有语义边界(preference 注入
71
- // 权重更高、decision/project 注入上下文不同),跨类型合并会丢类型信息,故
72
- // 默认禁止并在 skipInvalid 下被跳过;显式开启后放宽该检查,类型边界由用户
73
- // 自行承担(需与 dreamSkipInvalid 配合:开启后跨类型 merge 视为合法、可应用)。
74
- allowCrossTypeMerge: z.boolean().default(false),
75
45
  // Rule version for dream adjudication: when this bumps, older dream_runs
76
46
  // degrade to historical evidence (their receipts no longer drive live
77
47
  // decisions). Default 0 = no versioning in use yet.
@@ -189,43 +159,6 @@ export const Config = z.object({
189
159
  // Prefix/semantic search over entity names (used by recall).
190
160
  entitySearchEnabled: z.boolean().default(true),
191
161
 
192
- // --- wiki-link: explicit cross-memory [[links]] (v0.6.1) ----------------
193
- // Opt-in, off by default. When enabled, saveWithDedupe/update fire-and-forget
194
- // a wiki-link resolution pass: [[target]] / [[显示|target]] markers in a
195
- // memory's content become links_to relations in entity_relations (idempotent,
196
- // deduped by the unique relation index). The storage layer + read APIs
197
- // (getBacklinks/getForwardLinks/resolveWikiLink) are always available
198
- // regardless of this flag.
199
- wikiLinkEnabled: z.boolean().default(false),
200
-
201
- // --- tag system (v0.6.2) ---------------------------------------------------
202
- // Opt-in: when autoTagEnabled is true, a light LLM pass runs after each
203
- // autoDream consolidation and extracts 1-3 tags per retained memory
204
- // (autoTagMaxPerRun bounds how many memories are tagged per run). The tag
205
- // storage layer (store.setMemoryTags/getMemoryTags + tag: search + mirror
206
- // `#tag` line) is always available regardless of this flag.
207
- autoTagEnabled: z.boolean().default(false),
208
- autoTagMaxPerRun: z.natural().min(1).max(100).default(10),
209
- // Manual tagging (service.setMemoryTags / memory tools) is on by default;
210
- // set false to disable the manual write path too.
211
- manualTagEnabled: z.boolean().default(true),
212
-
213
- // --- sidebar trigger (issue #38) ---------------------------------------
214
- // The memory library is reachable from two UI surfaces: a "记忆库" tab in the
215
- // conversation header AND an entrance button at the sidebar footer (bottom
216
- // left). Some sidebars get crowded there — dsh-cost-meter and friends also
217
- // claim the footer slot — so the entrance button is now optional. Off just
218
- // hides the button; the conversation tab stays, so no functionality is lost.
219
- // The Web panel exposes this as a toggle (settings-over-config, see
220
- // settings.getUiConfig); the value here is the plugin-config default.
221
- showSidebarTrigger: z.boolean().default(true),
222
-
223
- // --- tag-weighted re-rank (v0.6.4) -------------------------------------
224
- // Opt-in: boost candidates whose tags overlap the query/session tags.
225
- tagBoostEnabled: z.boolean().default(false),
226
- tagBoostFactor: z.number().min(1).max(2).default(1.15),
227
- sessionTagBoostFactor: z.number().min(1).max(2).default(1.08),
228
-
229
162
  // --- sleep mode: idle-triggered deep maintenance (v0.4.0) ---------------
230
163
  // Opt-in, off by default. Unlike autoDream (threshold-triggered, lightweight)
231
164
  // sleep fires when the store has been quiet for sleepIdleMinutes and deep-
@@ -264,25 +197,14 @@ export const Config = z.object({
264
197
  sleepProvider: z.string().default(""),
265
198
  sleepModel: z.string().default(""),
266
199
  // Pass-through reasoning effort for sleep's LLM passes, same semantics as
267
- // dreamReasoningEffort: 'none' (default) omits the field; 'off' explicitly
268
- // disables thinking (thinking-type models would burn the whole budget on
269
- // reasoning); low/medium/high are forwarded verbatim.
200
+ // dreamReasoningEffort: 'none' (default) omits the field; low/medium/high
201
+ // are forwarded verbatim.
270
202
  sleepReasoningEffort: z.union([
271
- z.const("off"),
272
203
  z.const("low"),
273
204
  z.const("medium"),
274
205
  z.const("high"),
275
206
  z.const("none")
276
207
  ]).default("none"),
277
- // Batch entity extraction during sleep (issue #23). The write-path extractor
278
- // only fires when entityExtractionEnabled is on (an LLM call per write);
279
- // this additive phase backfills entities/attrs/relations for memories that
280
- // never went through it, so stores that leave the write-path extractor off
281
- // still accumulate an ego graph as long as sleep runs. On by default (it is
282
- // a no-op until a sleep cycle fires), capped per run to bound token spend.
283
- sleepEntityExtractionEnabled: z.boolean().default(true),
284
- // Max memories entity-extracted per sleep run (oldest un-extracted first).
285
- sleepEntityExtractionMaxPerRun: z.natural().min(1).max(100).default(20),
286
208
 
287
209
  // --- epistemic trust: memory source credibility (v0.4.5) -----------------
288
210
  // Distinguish memories by source: observation (measured / witnessed),
@@ -327,20 +249,4 @@ export const Config = z.object({
327
249
  // audits to recall_runs and NEVER touches recall_evals, regardless of this
328
250
  // flag (production isolation is unconditional).
329
251
  evalPersistTestResults: z.boolean().default(false),
330
-
331
- // --- heat: v0.7.0 self-evolution (heat + interest drift) ----------------
332
- // 总开关,默认开但保守:不改变召回排序,只提供热度字段 / sleep 降级
333
- // 联合判定保护 / 前端热度投影。关闭后跳过所有 heat 计算与热度触达。
334
- heatEnabled: z.boolean().default(true),
335
- // 幂律形状参数 α(heat = 1/(1+λΔt)^α),越大衰减越快。
336
- heatGlobalAlpha: z.number().min(0.1).max(5).default(1.2),
337
- // per-type 衰减因子 λ;λ=0 的类型免疫(热度恒 1.0,sleep 永不降级)。
338
- // 未知类型走默认 0.002。z.dict 的键为 type 字符串、值为数字 λ。
339
- heatTypeDecay: z.dict(z.number(), z.string()).default({ ...TYPE_DECAY_DEFAULTS }),
340
- // sleep 降级联合判定的热度下限:heat < 该值 且 importance<5 才允许降级。
341
- sleepHeatThreshold: z.number().min(0).max(1).default(0.05),
342
- // recordRecall 默认值(recall_runs 记录默认开;显式传 false 的调用方不受影响)。
343
- recallRecordDefault: z.boolean().default(true),
344
- // recall_runs 滚动清理保留天数。
345
- recallRetentionDays: z.natural().min(1).max(3650).default(90),
346
252
  });