@modusensus/dsh-mneme 0.6.5 → 0.6.6

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.
@@ -78,8 +78,12 @@ export async function runAutoTag({ ctx, service, config, route }) {
78
78
  ? config.autoTagMaxPerRun
79
79
  : 10;
80
80
  // Retained = post-consolidation active memories, newest first, capped.
81
+ // Skip memories that already carry live tags — otherwise every run re-picks
82
+ // the same "newest" batch (tagging doesn't bump updated_at) and older
83
+ // memories starve; manual tags are also left untouched this way.
81
84
  const memories = service.all()
82
85
  .filter((m) => !m.forgotten && !m.archived && !m.session_disposed_at && m.type !== "summary")
86
+ .filter((m) => (service.getMemoryTags?.(m.id) ?? []).length === 0)
83
87
  .sort((a, b) => {
84
88
  const ta = String(a.updated_at ?? "");
85
89
  const tb = String(b.updated_at ?? "");
@@ -137,7 +141,10 @@ export async function runAutoTag({ ctx, service, config, route }) {
137
141
  try {
138
142
  service.transaction(() => {
139
143
  for (const { id, tags } of toWrite) {
140
- service.applyMemoryTags(id, tags);
144
+ // Merge with existing live tags (never clobber manual tags that landed
145
+ // between selection and write).
146
+ const existing = service.getMemoryTags?.(id) ?? [];
147
+ service.applyMemoryTags(id, [...new Set([...existing, ...tags])]);
141
148
  tagged++;
142
149
  }
143
150
  });
package/lib/service.js CHANGED
@@ -373,7 +373,9 @@ export function createService({ store, mirror, config, onWrite, logger }) {
373
373
  .sort((a, b) => b.score - a.score)
374
374
  : hits.map((m) => ({ ...m, source: "tag" }));
375
375
  const result = hits.slice(0, topK);
376
- touchRecalled(result);
376
+ // Only count toward recall stats/forgetting when the caller asked for
377
+ // recording — the panel's `tag:` search must not pollute the curve.
378
+ if (options.recordRecall) touchRecalled(result);
377
379
  return result;
378
380
  }
379
381
 
package/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.6.5",
4
+ "version": "0.6.6",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -78,8 +78,12 @@ export async function runAutoTag({ ctx, service, config, route }) {
78
78
  ? config.autoTagMaxPerRun
79
79
  : 10;
80
80
  // Retained = post-consolidation active memories, newest first, capped.
81
+ // Skip memories that already carry live tags — otherwise every run re-picks
82
+ // the same "newest" batch (tagging doesn't bump updated_at) and older
83
+ // memories starve; manual tags are also left untouched this way.
81
84
  const memories = service.all()
82
85
  .filter((m) => !m.forgotten && !m.archived && !m.session_disposed_at && m.type !== "summary")
86
+ .filter((m) => (service.getMemoryTags?.(m.id) ?? []).length === 0)
83
87
  .sort((a, b) => {
84
88
  const ta = String(a.updated_at ?? "");
85
89
  const tb = String(b.updated_at ?? "");
@@ -137,7 +141,10 @@ export async function runAutoTag({ ctx, service, config, route }) {
137
141
  try {
138
142
  service.transaction(() => {
139
143
  for (const { id, tags } of toWrite) {
140
- service.applyMemoryTags(id, tags);
144
+ // Merge with existing live tags (never clobber manual tags that landed
145
+ // between selection and write).
146
+ const existing = service.getMemoryTags?.(id) ?? [];
147
+ service.applyMemoryTags(id, [...new Set([...existing, ...tags])]);
141
148
  tagged++;
142
149
  }
143
150
  });
package/src/service.js CHANGED
@@ -373,7 +373,9 @@ export function createService({ store, mirror, config, onWrite, logger }) {
373
373
  .sort((a, b) => b.score - a.score)
374
374
  : hits.map((m) => ({ ...m, source: "tag" }));
375
375
  const result = hits.slice(0, topK);
376
- touchRecalled(result);
376
+ // Only count toward recall stats/forgetting when the caller asked for
377
+ // recording — the panel's `tag:` search must not pollute the curve.
378
+ if (options.recordRecall) touchRecalled(result);
377
379
  return result;
378
380
  }
379
381
 
package/test/tag.test.js CHANGED
@@ -186,6 +186,24 @@ test("runAutoTag writes validated tags into entity_attrs", async () => {
186
186
  store.close();
187
187
  });
188
188
 
189
+ test("runAutoTag: skips already-tagged memories, merges into fresh ones", async () => {
190
+ const { store, service } = makeService();
191
+ const a = seed(service, "Linux", "内核");
192
+ const b = seed(service, "考研", "公共管理");
193
+ store.setMemoryTags(a.id, ["已有"]); // manual tag
194
+ const text = JSON.stringify([
195
+ { id: a.id, tags: ["linux"] },
196
+ { id: b.id, tags: ["考研"] }
197
+ ]);
198
+ const ctx = makeCtx([text]);
199
+ const result = await runAutoTag({ ctx, service, config: { dreamProvider: "d", dreamModel: "m" } });
200
+ assert.equal(result.ok, true);
201
+ assert.equal(result.tagged, 1, "only the untagged memory is tagged");
202
+ assert.deepEqual(store.getMemoryTags(a.id), ["已有"], "manual tags left untouched");
203
+ assert.deepEqual(store.getMemoryTags(b.id), ["考研"]);
204
+ store.close();
205
+ });
206
+
189
207
  test("runAutoTag fail-safe: garbage / aborted LLM output never throws and writes nothing", async () => {
190
208
  const { store, service } = makeService();
191
209
  const a = seed(service, "Alpha", "正文");