@modusensus/dsh-mneme 0.7.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -5
- package/dsh-mneme/CHANGELOG.md +9 -0
- package/dsh-mneme/README.md +3 -1
- package/dsh-mneme/lib/api.js +6 -3
- package/dsh-mneme/lib/dream.js +7 -2
- package/dsh-mneme/lib/index.js +7 -6
- package/dsh-mneme/lib/service.js +37 -3
- package/dsh-mneme/lib/settings.js +15 -6
- package/dsh-mneme/lib/store.js +5 -0
- package/dsh-mneme/package-lock.json +2 -2
- package/dsh-mneme/package.json +1 -1
- package/dsh-mneme/src/api.js +6 -3
- package/dsh-mneme/src/dream.js +7 -2
- package/dsh-mneme/src/index.js +7 -6
- package/dsh-mneme/src/service.js +37 -3
- package/dsh-mneme/src/settings.js +15 -6
- package/dsh-mneme/src/store.js +5 -0
- package/dsh-mneme/test/api.test.js +10 -3
- package/dsh-mneme/test/settings.test.js +6 -2
- package/dsh-mneme/test/tag.test.js +114 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -93,7 +93,7 @@ dsh web
|
|
|
93
93
|
## 🗺️ 路线图
|
|
94
94
|
|
|
95
95
|
```
|
|
96
|
-
🧬 基因(v0.3.0)→ 🛡️ 审计加固(v0.3.6–0.3.9)→ 💤 睡眠维护(v0.4.0)→ 🕸️ 召回融合与图谱(v0.5.0)→ ✨ 面板增强(v0.6.x)→ 🌡️ 自进化记忆(v0.7.0)
|
|
96
|
+
🧬 基因(v0.3.0)→ 🛡️ 审计加固(v0.3.6–0.3.9)→ 💤 睡眠维护(v0.4.0)→ 🕸️ 召回融合与图谱(v0.5.0)→ ✨ 面板增强(v0.6.x)→ 🌡️ 自进化记忆(v0.7.0)→ 🕸️ 图谱增强(v0.8.0)
|
|
97
97
|
```
|
|
98
98
|
|
|
99
99
|
| 版本 | 主题 | 状态 |
|
|
@@ -102,8 +102,9 @@ dsh web
|
|
|
102
102
|
| **v0.4.0** | Sleep Mode:空闲四阶段深度维护 | ✅ |
|
|
103
103
|
| **v0.5.0** | 召回融合与记忆可视化:BM25 + 图谱 + 热记忆 | ✅ |
|
|
104
104
|
| **v0.6.0** | 会话生命周期:删会话 ≠ 删记忆 | ✅ |
|
|
105
|
-
| **v0.7.0** |
|
|
106
|
-
| **v0.7.
|
|
105
|
+
| **v0.7.0** | 自进化记忆:heat 幂律衰减 + sleep 双保护 + 实体热投影 | ✅ |
|
|
106
|
+
| **v0.7.1** | issue #31 修复:tags 桥接 entity_attrs + autoTag 面板开关生效 | ✅ |
|
|
107
|
+
| **v0.8.0** | 图谱增强:兴趣漂移 + 跨 workspace 共享 | 🚧 计划中(9 月末) |
|
|
107
108
|
|
|
108
109
|
## 🧪 本地开发
|
|
109
110
|
|
|
@@ -194,7 +195,7 @@ Works out of the box. Enable these as needed:
|
|
|
194
195
|
## 🗺️ Roadmap
|
|
195
196
|
|
|
196
197
|
```
|
|
197
|
-
🧬 Gene (v0.3.0) → 🛡️ Audit hardening (v0.3.6–0.3.9) → 💤 Sleep maintenance (v0.4.0) → 🕸️ Recall fusion & graph (v0.5.0) → ✨ Panel enhancements (v0.6.x) → 🌡️ Self-evolving memory (v0.7.0)
|
|
198
|
+
🧬 Gene (v0.3.0) → 🛡️ Audit hardening (v0.3.6–0.3.9) → 💤 Sleep maintenance (v0.4.0) → 🕸️ Recall fusion & graph (v0.5.0) → ✨ Panel enhancements (v0.6.x) → 🌡️ Self-evolving memory (v0.7.0) → 🕸️ Graph enhancement (v0.8.0)
|
|
198
199
|
```
|
|
199
200
|
|
|
200
201
|
| Version | Theme | Status |
|
|
@@ -203,7 +204,9 @@ Works out of the box. Enable these as needed:
|
|
|
203
204
|
| **v0.4.0** | Sleep Mode: idle 4-phase deep maintenance | ✅ |
|
|
204
205
|
| **v0.5.0** | Recall fusion & visualization: BM25 + graph + hot memory | ✅ |
|
|
205
206
|
| **v0.6.0** | Session lifecycle: delete session ≠ delete memories | ✅ |
|
|
206
|
-
| **v0.7.0
|
|
207
|
+
| **v0.7.0** | Self-evolving memory: heat decay + sleep dual-protection + entity heat projection | ✅ |
|
|
208
|
+
| **v0.7.1** | Issue #31 fix: tags bridged to entity_attrs + autoTag panel toggle takes effect | ✅ |
|
|
209
|
+
| **v0.8.0** | Graph enhancement: interest drift + cross-workspace sharing | 🚧 Planned (late Sep) |
|
|
207
210
|
|
|
208
211
|
## 🧪 Local Development
|
|
209
212
|
|
package/dsh-mneme/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.7.1] - 2026-08-24
|
|
4
|
+
|
|
5
|
+
### 🐛 修复(issue #31)
|
|
6
|
+
|
|
7
|
+
- **memory_save / memory_update 的 tags 桥接进 entity_attrs 标签存储**:工具传入的 tags 之前只写 `memories.tags` 列,目录视图(`getDirectory`)/ `tag:` 检索 / tagBoost 从 `entity_attrs`(attr_key='tags' AND valid_until IS NULL)读取,导致看不到;现在 `saveWithDedupe` 创建/合并分支与 `service.update` 在写入后同步调 `store.setMemoryTags(id, tags)`,显式 `tags: []` 会把记忆移回 untagged
|
|
8
|
+
- **`store.setMemoryTags` 反向同步 `memories.tags` 列**:手动/autoTag 打标后,搜索结果与 API 返回的 `memory.tags` 不再和目录漂移
|
|
9
|
+
- **autoTag 面板开关成为运行时真正的消费方**:dream 的 autoTag 判定与 `service.setMemoryTags` 的 manual 门禁改为读取「settings 覆盖合并 plugin config」的有效值;`getAutoTagConfig()` 未存储键返回 `null` 而非 `false`,`setAutoTagConfig` 只持久化用户实际触碰的键(部分更新不再误关另一开关);`manualTagEnabled` 默认统一为插件配置的 `true`
|
|
10
|
+
- 新增 7 个回归测试,全套 764 通过
|
|
11
|
+
|
|
3
12
|
## [0.7.0] - 2026-08-24
|
|
4
13
|
|
|
5
14
|
### 🆕 自进化记忆(heat 热度模型)
|
package/dsh-mneme/README.md
CHANGED
|
@@ -241,6 +241,7 @@ v0.3.0 起新增**记忆基因**层:从记忆里抽取**命名实体**、**带
|
|
|
241
241
|
|
|
242
242
|
| 版本 | 亮点 |
|
|
243
243
|
|------|------|
|
|
244
|
+
| **v0.7.1** | issue #31 修复:memory_save/memory_update 的 tags 桥接进 entity_attrs 标签存储(目录/`tag:` 检索/tagBoost 立即可见,`tags: []` 清空移回 untagged)+ `store.setMemoryTags` 反向同步 `memories.tags` 列 + autoTag 面板开关成为运行时消费方(settings 覆盖 config);764 测试全绿 |
|
|
244
245
|
| **v0.7.0** | 自进化记忆(heat 热度模型 + per-type 差异化半衰期 + sleep 热联合双保护)+ updated_at 语义修正(不算访问)+ recall_runs injected 两档标记 + 90 天滚动清理 + 实体热投影(ego-graph node heat → 前端节点大小/明暗);757 测试全绿 |
|
|
245
246
|
| **v0.6.11** | 社区修复(PR #27,Jstn-1g):memory 渲染器暴露记忆 ID + 防御性加固(条数/块预算/Unicode 截断/JSONL 注入防护);issue #14 已关闭;735 测试全绿 |
|
|
246
247
|
| **v0.6.10** | 记忆面板卡片布局品质优化:清理死 CSS + 合并 `.mneme-xmain` 双定义 + 补无障碍(分类按钮 `aria-pressed`、三卡 `role=region`+`aria-label`、搜索框 `aria-label`);723 测试全绿 |
|
|
@@ -285,7 +286,8 @@ v0.3.0 起新增**记忆基因**层:从记忆里抽取**命名实体**、**带
|
|
|
285
286
|
| **v0.6.3** | ✅ 完成 | 目录视图 | Tag 文件夹手风琴 + 无标签兜底 + 点击跳详情 + `GET /api/dsh-mneme/directory` 端点 |
|
|
286
287
|
| **v0.6.4** | ✅ 完成 | Tag 加权召回 | query/session tag 交集 boost(×1.15 / ×1.08),`tagBoostEnabled` 默认关 |
|
|
287
288
|
| **v0.7.0** | ✅ 完成 | 自进化记忆 | heat 幂律衰减 + per-type 差异化半衰期(TYPE_DECAY)+ sleep 热联合双保护 + updated_at 语义修正 + recall_runs injected 两档标记 + 90 天清理 + 实体热投影(前端节点大小/明暗);757 测试全绿 |
|
|
288
|
-
| **v0.7.
|
|
289
|
+
| **v0.7.1** | ✅ 完成 | issue #31 修复 | memory_save/update tags 桥接 entity_attrs 标签存储 + 列反向同步 + autoTag 面板开关生效(settings 覆盖 config);764 测试全绿 |
|
|
290
|
+
| **v0.8.0** | 🚧 计划中(9 月末) | 图谱增强 | 兴趣漂移可视化 + 跨 workspace 记忆共享 + 更多 heat 信号 |
|
|
289
291
|
|
|
290
292
|
> 新能力一律做成**可开关的功能**(配置启用/关闭),默认保守开启、不破坏现有行为。`failure_memories` 表与 autoDream 决策引擎已为后续反思性成长铺好路。
|
|
291
293
|
|
package/dsh-mneme/lib/api.js
CHANGED
|
@@ -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
|
-
|
|
247
|
-
|
|
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:
|
|
253
|
+
sendJson(res, 200, { config: service.getTagConfig() });
|
|
251
254
|
} catch {
|
|
252
255
|
sendJson(res, 500, { error: "internal" });
|
|
253
256
|
}
|
package/dsh-mneme/lib/dream.js
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/dsh-mneme/lib/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/dsh-mneme/lib/service.js
CHANGED
|
@@ -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 (
|
|
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: () =>
|
|
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 ===
|
|
153
|
-
manualTagEnabled: stored.manualTagEnabled ===
|
|
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
|
-
|
|
166
|
-
|
|
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
|
}
|
package/dsh-mneme/lib/store.js
CHANGED
|
@@ -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");
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@modusensus/dsh-mneme",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@modusensus/dsh-mneme",
|
|
9
|
-
"version": "0.7.
|
|
9
|
+
"version": "0.7.1",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@huggingface/transformers": "^4.2.0"
|
package/dsh-mneme/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@modusensus/dsh-mneme",
|
|
3
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.
|
|
4
|
+
"version": "0.7.1",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
package/dsh-mneme/src/api.js
CHANGED
|
@@ -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
|
-
|
|
247
|
-
|
|
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:
|
|
253
|
+
sendJson(res, 200, { config: service.getTagConfig() });
|
|
251
254
|
} catch {
|
|
252
255
|
sendJson(res, 500, { error: "internal" });
|
|
253
256
|
}
|
package/dsh-mneme/src/dream.js
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/dsh-mneme/src/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/dsh-mneme/src/service.js
CHANGED
|
@@ -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 (
|
|
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: () =>
|
|
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 ===
|
|
153
|
-
manualTagEnabled: stored.manualTagEnabled ===
|
|
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
|
-
|
|
166
|
-
|
|
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
|
}
|
package/dsh-mneme/src/store.js
CHANGED
|
@@ -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");
|
|
@@ -29,8 +29,10 @@ function req(path, method = "GET", body = null) {
|
|
|
29
29
|
|
|
30
30
|
function setup(embedder, apiToken = "") {
|
|
31
31
|
const store = createStore(":memory:");
|
|
32
|
-
const service = createService({ store, mirror: null, config: {} });
|
|
33
32
|
const settings = createSettings(store.db);
|
|
33
|
+
// settings is threaded into the service so the /config endpoint reports the
|
|
34
|
+
// effective settings-over-config tag toggles (issue #31).
|
|
35
|
+
const service = createService({ store, mirror: null, config: {}, settings });
|
|
34
36
|
const commands = {
|
|
35
37
|
add: (def) => settings.addCommand(def),
|
|
36
38
|
remove: (id) => settings.removeCommand(id),
|
|
@@ -554,7 +556,9 @@ test("GET/PUT /api/dsh-mneme/config round-trips autoTag switches", async () => {
|
|
|
554
556
|
const res1 = new FakeRes();
|
|
555
557
|
await route.handler(req("/api/dsh-mneme/config"), res1);
|
|
556
558
|
assert.equal(res1.statusCode, 200);
|
|
557
|
-
|
|
559
|
+
// unset → effective config falls back to plugin-config defaults:
|
|
560
|
+
// autoTagEnabled=false, manualTagEnabled=true (issue #31 default unification)
|
|
561
|
+
assert.deepEqual(JSON.parse(res1.body).config, { autoTagEnabled: false, manualTagEnabled: true });
|
|
558
562
|
const res2 = new FakeRes();
|
|
559
563
|
await route.handler(req("/api/dsh-mneme/config", "PUT", { autoTagEnabled: true, manualTagEnabled: false }), res2);
|
|
560
564
|
assert.equal(res2.statusCode, 200);
|
|
@@ -590,5 +594,8 @@ test("PUT /api/dsh-mneme/config supports partial update", async () => {
|
|
|
590
594
|
const res = new FakeRes();
|
|
591
595
|
await route.handler(req("/api/dsh-mneme/config", "PUT", { autoTagEnabled: true }), res);
|
|
592
596
|
assert.equal(res.statusCode, 200);
|
|
593
|
-
|
|
597
|
+
// Partial PUT only stores autoTagEnabled; manualTagEnabled stays unset and
|
|
598
|
+
// falls back to the plugin-config default (true) — a lone autoTag toggle must
|
|
599
|
+
// never silently disable manual tagging (issue #31).
|
|
600
|
+
assert.deepEqual(JSON.parse(res.body).config, { autoTagEnabled: true, manualTagEnabled: true });
|
|
594
601
|
});
|
|
@@ -100,11 +100,15 @@ test("vector config disabled value is stored as false", () => {
|
|
|
100
100
|
store.close();
|
|
101
101
|
});
|
|
102
102
|
|
|
103
|
-
test("autoTag config defaults to
|
|
103
|
+
test("autoTag config defaults to null (unset) and round-trips only explicit keys", () => {
|
|
104
104
|
const { store, settings } = setup();
|
|
105
|
-
|
|
105
|
+
// Issue #31: an unset key is null so runtime consumers fall back to the
|
|
106
|
+
// plugin config default — the settings layer must not invent `false`.
|
|
107
|
+
assert.deepEqual(settings.getAutoTagConfig(), { autoTagEnabled: null, manualTagEnabled: null });
|
|
106
108
|
settings.setAutoTagConfig({ autoTagEnabled: true, manualTagEnabled: true });
|
|
107
109
|
assert.deepEqual(settings.getAutoTagConfig(), { autoTagEnabled: true, manualTagEnabled: true });
|
|
110
|
+
// A partial update only touches the key it was given: manualTagEnabled was
|
|
111
|
+
// stored earlier so it keeps its value (true), it does not revert to null.
|
|
108
112
|
settings.setAutoTagConfig({ autoTagEnabled: false });
|
|
109
113
|
assert.deepEqual(settings.getAutoTagConfig(), { autoTagEnabled: false, manualTagEnabled: true });
|
|
110
114
|
store.close();
|
|
@@ -12,6 +12,7 @@ import { join } from "node:path";
|
|
|
12
12
|
import { createStore } from "../src/store.js";
|
|
13
13
|
import { createService } from "../src/service.js";
|
|
14
14
|
import { createMirror } from "../src/mirror.js";
|
|
15
|
+
import { createSettings } from "../src/settings.js";
|
|
15
16
|
import { parseTags, sanitizeTags, MAX_TAG_LENGTH } from "../src/parser/tag.js";
|
|
16
17
|
import { runAutoTag } from "../src/dream/tag-extractor.js";
|
|
17
18
|
import { createDreamScheduler } from "../src/dream.js";
|
|
@@ -310,3 +311,116 @@ test("service.setMemoryTags respects manualTagEnabled=false gate", () => {
|
|
|
310
311
|
assert.equal(r.ok, false);
|
|
311
312
|
assert.match(r.error, /manualTagEnabled/);
|
|
312
313
|
});
|
|
314
|
+
|
|
315
|
+
// ============================================================ issue #31 fixes
|
|
316
|
+
|
|
317
|
+
test("memory_save with tags bridges into the entity_attrs tag store (issue #31)", () => {
|
|
318
|
+
const { store, service } = makeService();
|
|
319
|
+
const { memory } = service.saveWithDedupe({
|
|
320
|
+
type: "project", title: "桥接", content: "c", tags: ["linux", "考研"]
|
|
321
|
+
});
|
|
322
|
+
assert.deepEqual(store.getMemoryTags(memory.id), ["linux", "考研"], "tool-passed tags land in entity_attrs");
|
|
323
|
+
assert.deepEqual(store.getMemoryTagsMap([memory.id]).get(memory.id), ["linux", "考研"], "tag map sees them");
|
|
324
|
+
const dir = store.getDirectory();
|
|
325
|
+
assert.deepEqual(dir.groups.map((g) => g.tag).sort(), ["linux", "考研"], "directory groups by tool-passed tags");
|
|
326
|
+
assert.equal(dir.untagged.length, 0, "not dumped in the untagged bucket");
|
|
327
|
+
assert.ok(store.findMemoriesByTags(["linux"]).some((m) => m.id === memory.id), "tag: recall finds it");
|
|
328
|
+
store.close();
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test("saveWithDedupe merge with tags bridges into the tag store too (issue #31)", () => {
|
|
332
|
+
const { store, service } = makeService();
|
|
333
|
+
const { memory } = service.saveWithDedupe({
|
|
334
|
+
type: "project", title: "同题", content: "第一段", tags: ["linux"]
|
|
335
|
+
});
|
|
336
|
+
assert.deepEqual(store.getMemoryTags(memory.id), ["linux"]);
|
|
337
|
+
const { action, memory: merged } = service.saveWithDedupe({
|
|
338
|
+
type: "project", title: "同题", content: "第二段", tags: ["bash"]
|
|
339
|
+
});
|
|
340
|
+
assert.equal(action, "merged");
|
|
341
|
+
assert.deepEqual(store.getMemoryTags(merged.id), ["bash"], "merge overwrites tags and bridges them");
|
|
342
|
+
store.close();
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test("saveWithDedupe merge with empty tags clears stale entity_attrs tags (issue #31)", () => {
|
|
346
|
+
const { store, service } = makeService();
|
|
347
|
+
const { memory } = service.saveWithDedupe({ type: "project", title: "清空", content: "第一段", tags: ["old"] });
|
|
348
|
+
assert.deepEqual(store.getMemoryTags(memory.id), ["old"]);
|
|
349
|
+
const { action, memory: merged } = service.saveWithDedupe({
|
|
350
|
+
type: "project", title: "清空", content: "第二段", tags: []
|
|
351
|
+
});
|
|
352
|
+
assert.equal(action, "merged");
|
|
353
|
+
assert.deepEqual(store.getMemoryTags(merged.id), [], "empty tags on merge invalidate the stale live row");
|
|
354
|
+
const dir = store.getDirectory();
|
|
355
|
+
assert.equal(dir.groups.length, 0, "directory no longer shows the stale tag");
|
|
356
|
+
assert.equal(dir.untagged.length, 1, "memory back to untagged");
|
|
357
|
+
store.close();
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
test("memory_update (service.update) with tags bridges and clearing moves to untagged (issue #31)", () => {
|
|
361
|
+
const { store, service } = makeService();
|
|
362
|
+
const { memory } = service.saveWithDedupe({ type: "project", title: "更新", content: "c", tags: ["old"] });
|
|
363
|
+
const updated = service.update(memory.id, { tags: ["new", "考研"] });
|
|
364
|
+
assert.deepEqual(store.getMemoryTags(updated.id), ["new", "考研"], "update bridges new tags");
|
|
365
|
+
assert.ok(store.getDirectory().groups.some((g) => g.tag === "new"), "directory reflects update tags");
|
|
366
|
+
service.update(updated.id, { tags: [] });
|
|
367
|
+
assert.deepEqual(store.getMemoryTags(updated.id), [], "clearing removes the live row");
|
|
368
|
+
const dir = store.getDirectory();
|
|
369
|
+
assert.equal(dir.groups.length, 0, "no tag groups after clearing");
|
|
370
|
+
assert.equal(dir.untagged.length, 1, "memory back to untagged");
|
|
371
|
+
store.close();
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
test("setMemoryTags keeps the memories.tags column in sync with entity_attrs (issue #31)", () => {
|
|
375
|
+
const { store, service } = makeService();
|
|
376
|
+
const { memory } = service.saveWithDedupe({ type: "project", title: "列同步", content: "c" });
|
|
377
|
+
service.setMemoryTags(memory.id, ["linux"]);
|
|
378
|
+
assert.deepEqual(store.getById(memory.id).tags, ["linux"], "column mirrors the live tag row");
|
|
379
|
+
service.setMemoryTags(memory.id, []);
|
|
380
|
+
assert.deepEqual(store.getById(memory.id).tags, [], "clearing also clears the column");
|
|
381
|
+
store.close();
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test("runDream auto-tag respects the settings toggle over plugin config (issue #31)", async () => {
|
|
385
|
+
const store = createStore(":memory:");
|
|
386
|
+
const settings = createSettings(store.db);
|
|
387
|
+
settings.setAutoTagConfig({ autoTagEnabled: true });
|
|
388
|
+
// plugin config says auto-tagging is OFF; the settings toggle flips it on.
|
|
389
|
+
const service = createService({ store, mirror: null, config: { autoTagEnabled: false }, settings });
|
|
390
|
+
const a = service.saveWithDedupe({ type: "preference", title: "旧1", content: "第一段" }).memory;
|
|
391
|
+
const b = service.saveWithDedupe({ type: "preference", title: "旧2", content: "第二段" }).memory;
|
|
392
|
+
const keep = (id) => JSON.stringify([{ action: "keep", ids: [id] }]);
|
|
393
|
+
let calls = 0;
|
|
394
|
+
const ctx = {
|
|
395
|
+
llm: {
|
|
396
|
+
stream: async function* () {
|
|
397
|
+
calls++;
|
|
398
|
+
if (calls === 1) yield { type: "text-delta", text: keep(a.id) }; // consolidation
|
|
399
|
+
else if (calls === 2) yield { type: "text-delta", text: JSON.stringify([{ id: a.id, tags: ["内核"] }]) }; // auto-tag
|
|
400
|
+
else yield { type: "text-delta", text: "总览" }; // summary
|
|
401
|
+
yield { type: "finish", reason: { kind: "ok" } };
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
logger: { warn: () => {}, info: () => {} }
|
|
405
|
+
};
|
|
406
|
+
const dream = createDreamScheduler({ thresholdCount: 1, thresholdChars: 0, delayMs: 0 });
|
|
407
|
+
const result = await dream.runDream(ctx, service, {
|
|
408
|
+
dreamProvider: "deepseek", dreamModel: "deepseek-chat", autoTagEnabled: false
|
|
409
|
+
}, settings);
|
|
410
|
+
assert.equal(result.ok, true);
|
|
411
|
+
assert.deepEqual(store.getMemoryTags(a.id), ["内核"], "settings toggle enabled auto-tag despite config.autoTagEnabled=false");
|
|
412
|
+
store.close();
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
test("service.setMemoryTags manual gate follows the settings toggle (issue #31)", () => {
|
|
416
|
+
const store = createStore(":memory:");
|
|
417
|
+
const settings = createSettings(store.db);
|
|
418
|
+
settings.setAutoTagConfig({ manualTagEnabled: false });
|
|
419
|
+
const service = createService({ store, mirror: null, config: {}, settings });
|
|
420
|
+
const a = service.saveWithDedupe({ type: "preference", title: "Alpha", content: "正文" }).memory;
|
|
421
|
+
const r = service.setMemoryTags(a.id, ["linux"]);
|
|
422
|
+
assert.equal(r.ok, false, "settings gate blocks manual tagging");
|
|
423
|
+
assert.match(r.error, /manualTagEnabled/);
|
|
424
|
+
assert.equal(service.manualTagEnabled(), false, "gate flag reflects the settings override");
|
|
425
|
+
store.close();
|
|
426
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@modusensus/dsh-mneme",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Structured memory engine for DeepSeek Harness. Offline semantic search, entity-attribute-timeline, autoDream self-consolidation, and human-editable Markdown storage.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|