@modusensus/dsh-mneme 0.7.10 → 0.7.12
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.en.md +437 -0
- package/README.md +364 -166
- package/bin/cli.mjs +603 -0
- package/{dsh-mneme/cordis.patch.yml → cordis.patch.yml} +1 -0
- package/lib/api-standalone.js +264 -0
- package/{dsh-mneme/lib → lib}/api.js +133 -289
- package/{dsh-mneme/lib → lib}/client.js +1008 -922
- package/{dsh-mneme/lib → lib}/config.js +54 -99
- package/{dsh-mneme/lib → lib}/dream/decisions.js +94 -143
- package/{dsh-mneme/lib → lib}/dream/sleep.js +12 -152
- package/{dsh-mneme/lib → lib}/dream.js +22 -168
- package/{dsh-mneme/src → lib}/hot-memory.js +0 -7
- package/{dsh-mneme/lib → lib}/index.js +45 -42
- package/{dsh-mneme/lib → lib}/inject.js +11 -53
- package/{dsh-mneme/src → lib}/mirror.js +1 -12
- package/{dsh-mneme/lib → lib}/quality-filter.js +1 -3
- package/{dsh-mneme/lib → lib}/reranker.js +1 -4
- package/{dsh-mneme/lib → lib}/service.js +20 -410
- package/{dsh-mneme/src → lib}/settings.js +34 -64
- package/{dsh-mneme/lib → lib}/store.js +38 -450
- package/{dsh-mneme/lib → lib}/summarize.js +10 -10
- package/lib/tools.js +274 -0
- package/package.json +45 -19
- package/{dsh-mneme/scripts → scripts}/e2e-dsh.js +3 -4
- package/{dsh-mneme/scripts → scripts}/sync-lib.js +5 -10
- package/src/api-standalone.js +264 -0
- package/{dsh-mneme/src → src}/api.js +133 -289
- package/{dsh-mneme/src → src}/config.js +54 -99
- package/{dsh-mneme/src → src}/dream/decisions.js +94 -143
- package/{dsh-mneme/src → src}/dream/sleep.js +12 -152
- package/{dsh-mneme/src → src}/dream.js +22 -168
- package/{dsh-mneme/lib → src}/hot-memory.js +0 -7
- package/{dsh-mneme/src → src}/index.js +45 -42
- package/{dsh-mneme/src → src}/inject.js +11 -53
- package/{dsh-mneme/lib → src}/mirror.js +1 -12
- package/{dsh-mneme/src → src}/quality-filter.js +1 -3
- package/{dsh-mneme/src → src}/reranker.js +1 -4
- package/{dsh-mneme/src → src}/service.js +20 -410
- package/{dsh-mneme/lib → src}/settings.js +34 -64
- package/{dsh-mneme/src → src}/store.js +38 -450
- package/{dsh-mneme/src → src}/summarize.js +10 -10
- package/src/tools.js +274 -0
- package/{dsh-mneme/test → test}/api.test.js +45 -156
- package/{dsh-mneme/test → test}/client.test.js +10 -206
- package/{dsh-mneme/test → test}/config.test.js +0 -21
- package/{dsh-mneme/test → test}/dream.test.js +1 -160
- package/{dsh-mneme/test → test}/graph-api.test.js +0 -34
- package/{dsh-mneme/test → test}/hot-memory.test.js +0 -29
- package/test/inject.test.js +120 -0
- package/{dsh-mneme/test → test}/llm-audit.test.js +4 -4
- package/{dsh-mneme/test → test}/reasoning-effort.test.js +0 -27
- package/{dsh-mneme/test → test}/recall-layer.test.js +5 -26
- package/{dsh-mneme/test → test}/reranker.test.js +0 -43
- package/{dsh-mneme/test → test}/service-search.test.js +0 -25
- package/{dsh-mneme/test → test}/service.test.js +0 -106
- package/{dsh-mneme/test → test}/settings.test.js +16 -35
- package/{dsh-mneme/test → test}/sleep.test.js +4 -171
- package/test/standalone-api.test.js +326 -0
- package/{dsh-mneme/test → test}/store.test.js +0 -76
- package/{dsh-mneme/test → test}/summarize.test.js +16 -15
- package/test/tools.test.js +265 -0
- package/.github/workflows/publish.yml +0 -58
- package/.github/workflows/test.yml +0 -32
- package/CHANGELOG.md +0 -91
- package/CONTRIBUTING.md +0 -293
- package/SECURITY.md +0 -718
- package/docs/devlog/2026-08-14-dsh-mneme-dev-log.md +0 -247
- package/docs/devlog/2026-08-15-dsh-mneme-audit-stress-dev-log.md +0 -145
- package/docs/devlog/2026-08-15-dsh-mneme-pipeline-dev-log.md +0 -56
- package/docs/devlog/2026-08-15-dsh-mneme-reflection-dev-log.md +0 -77
- package/docs/devlog/2026-08-15-dsh-mneme-review-fixes-dev-log.md +0 -64
- package/docs/devlog/2026-08-15-dsh-mneme-semantic-dev-log.md +0 -90
- package/dsh-mneme/CHANGELOG.md +0 -419
- package/dsh-mneme/LICENSE +0 -21
- package/dsh-mneme/README.md +0 -501
- package/dsh-mneme/docs/AGENT_MEMORY_RESEARCH.md +0 -183
- package/dsh-mneme/docs/ENTITIES.md +0 -245
- package/dsh-mneme/docs/LOCAL_MODEL.md +0 -141
- package/dsh-mneme/docs/MIGRATION.md +0 -127
- package/dsh-mneme/docs/SEMANTIC.md +0 -256
- package/dsh-mneme/docs/SLEEP.md +0 -163
- package/dsh-mneme/lib/dream/tag-extractor.js +0 -156
- package/dsh-mneme/lib/heat.js +0 -136
- package/dsh-mneme/lib/parser/tag.js +0 -59
- package/dsh-mneme/lib/parser/wiki-link.js +0 -38
- package/dsh-mneme/lib/search/tag-boost.js +0 -61
- package/dsh-mneme/lib/tools.js +0 -458
- package/dsh-mneme/package-lock.json +0 -1936
- package/dsh-mneme/package.json +0 -83
- package/dsh-mneme/scripts/check-sync.js +0 -42
- package/dsh-mneme/src/client.js +0 -2242
- package/dsh-mneme/src/dream/tag-extractor.js +0 -156
- package/dsh-mneme/src/heat.js +0 -136
- package/dsh-mneme/src/parser/tag.js +0 -59
- package/dsh-mneme/src/parser/wiki-link.js +0 -38
- package/dsh-mneme/src/search/tag-boost.js +0 -61
- package/dsh-mneme/src/tools.js +0 -458
- package/dsh-mneme/test/boundary-v0625.test.js +0 -82
- package/dsh-mneme/test/directory.test.js +0 -134
- package/dsh-mneme/test/heat.test.js +0 -148
- package/dsh-mneme/test/inject.test.js +0 -206
- package/dsh-mneme/test/layered-types-stats.test.js +0 -144
- package/dsh-mneme/test/lib-smoke.test.js +0 -109
- package/dsh-mneme/test/normalize-decisions.test.js +0 -120
- package/dsh-mneme/test/provenance.test.js +0 -103
- package/dsh-mneme/test/recall-runs.test.js +0 -93
- package/dsh-mneme/test/sleep-heat.test.js +0 -112
- package/dsh-mneme/test/tag-boost.test.js +0 -125
- package/dsh-mneme/test/tag.test.js +0 -426
- package/dsh-mneme/test/tools.test.js +0 -674
- package/dsh-mneme/test/updated-at-semantics.test.js +0 -113
- package/dsh-mneme/test/wiki-link.test.js +0 -332
- package//346/250/252/345/271/205.png +0 -0
- /package/{dsh-mneme/lib → lib}/commands.js +0 -0
- /package/{dsh-mneme/lib → lib}/dream/clustering.js +0 -0
- /package/{dsh-mneme/lib → lib}/embedding.js +0 -0
- /package/{dsh-mneme/lib → lib}/entities/extractor.js +0 -0
- /package/{dsh-mneme/lib → lib}/local-embedder.js +0 -0
- /package/{dsh-mneme/lib → lib}/search/adaptive.js +0 -0
- /package/{dsh-mneme/lib → lib}/search/bm25.js +0 -0
- /package/{dsh-mneme/lib → lib}/vector-index.js +0 -0
- /package/{dsh-mneme/scripts → scripts}/benchmark-embed.js +0 -0
- /package/{dsh-mneme/scripts → scripts}/benchmark-recall.js +0 -0
- /package/{dsh-mneme/scripts → scripts}/benchmark-rerank.js +0 -0
- /package/{dsh-mneme/scripts → scripts}/stress-dsh.js +0 -0
- /package/{dsh-mneme/src → src}/commands.js +0 -0
- /package/{dsh-mneme/src → src}/dream/clustering.js +0 -0
- /package/{dsh-mneme/src → src}/embedding.js +0 -0
- /package/{dsh-mneme/src → src}/entities/extractor.js +0 -0
- /package/{dsh-mneme/src → src}/local-embedder.js +0 -0
- /package/{dsh-mneme/src → src}/search/adaptive.js +0 -0
- /package/{dsh-mneme/src → src}/search/bm25.js +0 -0
- /package/{dsh-mneme/src → src}/vector-index.js +0 -0
- /package/{dsh-mneme/test → test}/audit.test.js +0 -0
- /package/{dsh-mneme/test → test}/benchmark.test.js +0 -0
- /package/{dsh-mneme/test → test}/clustering.test.js +0 -0
- /package/{dsh-mneme/test → test}/commands.test.js +0 -0
- /package/{dsh-mneme/test → test}/conflict-freeze.test.js +0 -0
- /package/{dsh-mneme/test → test}/entities.test.js +0 -0
- /package/{dsh-mneme/test → test}/epistemic.test.js +0 -0
- /package/{dsh-mneme/test → test}/fnew-0112.test.js +0 -0
- /package/{dsh-mneme/test → test}/fnew-03.test.js +0 -0
- /package/{dsh-mneme/test → test}/helpers/dream-mock.js +0 -0
- /package/{dsh-mneme/test → test}/local-embedder.test.js +0 -0
- /package/{dsh-mneme/test → test}/mirror-dirty.test.js +0 -0
- /package/{dsh-mneme/test → test}/mirror-edit-digest.test.js +0 -0
- /package/{dsh-mneme/test → test}/mirror-generation.test.js +0 -0
- /package/{dsh-mneme/test → test}/mirror.test.js +0 -0
- /package/{dsh-mneme/test → test}/peer-blockers.test.js +0 -0
- /package/{dsh-mneme/test → test}/policy-epoch.test.js +0 -0
- /package/{dsh-mneme/test → test}/quality-filter.test.js +0 -0
- /package/{dsh-mneme/test → test}/recall-evals.test.js +0 -0
- /package/{dsh-mneme/test → test}/receipt-chain.test.js +0 -0
- /package/{dsh-mneme/test → test}/reflection.test.js +0 -0
- /package/{dsh-mneme/test → test}/search-fusion.test.js +0 -0
- /package/{dsh-mneme/test → test}/semantic.test.js +0 -0
- /package/{dsh-mneme/test → test}/stress.test.js +0 -0
- /package/{dsh-mneme/test → test}/vector-index.test.js +0 -0
|
@@ -3,11 +3,8 @@ import { TYPE_FILE } from "./mirror.js";
|
|
|
3
3
|
import { evaluateMemoryQuality } from "./quality-filter.js";
|
|
4
4
|
import { createBM25Index } from "./search/bm25.js";
|
|
5
5
|
import { adaptiveThreshold } from "./search/adaptive.js";
|
|
6
|
-
import { parseWikiLinks } from "./parser/wiki-link.js";
|
|
7
|
-
import { extractQueryTags, applyTagBoost } from "./search/tag-boost.js";
|
|
8
|
-
import { computeHeat } from "./heat.js";
|
|
9
6
|
|
|
10
|
-
const INJECT_TYPES = new Set(["preference", "project", "decision", "summary"
|
|
7
|
+
const INJECT_TYPES = new Set(["preference", "project", "decision", "summary"]);
|
|
11
8
|
|
|
12
9
|
// Epistemic trust weights (v0.4.5): when config.trustEpistemicWeighting is on,
|
|
13
10
|
// each recall candidate's existing score is multiplied by the weight of its
|
|
@@ -65,7 +62,7 @@ export function computeRetrievalMetrics(actualIds, expectedIds) {
|
|
|
65
62
|
};
|
|
66
63
|
}
|
|
67
64
|
|
|
68
|
-
export function createService({ store, mirror, config, onWrite, logger
|
|
65
|
+
export function createService({ store, mirror, config, onWrite, logger }) {
|
|
69
66
|
// Optional dream scheduler hook, installed via setDreamHook after creation
|
|
70
67
|
// (the scheduler holds a reference back to the service, so it cannot be
|
|
71
68
|
// passed in the constructor). Fired on the same write events as onWrite.
|
|
@@ -91,33 +88,6 @@ export function createService({ store, mirror, config, onWrite, logger, settings
|
|
|
91
88
|
let vectorIndex = null;
|
|
92
89
|
let reranker = null;
|
|
93
90
|
|
|
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
|
-
|
|
108
|
-
// App-wide effective config for the Web panel (/api/dsh-mneme/config): the
|
|
109
|
-
// tag toggles plus UI preferences (issue #38). Settings overrides plugin
|
|
110
|
-
// config when explicitly stored; unset keys fall back to the zod default.
|
|
111
|
-
const appConfig = () => {
|
|
112
|
-
const stored = settings?.getUiConfig?.() ?? {};
|
|
113
|
-
return {
|
|
114
|
-
...tagConfig(),
|
|
115
|
-
showSidebarTrigger: typeof stored.showSidebarTrigger === "boolean"
|
|
116
|
-
? stored.showSidebarTrigger
|
|
117
|
-
: (config.showSidebarTrigger ?? true)
|
|
118
|
-
};
|
|
119
|
-
};
|
|
120
|
-
|
|
121
91
|
// Optional recall recorder, installed via setRecallRecorder after creation.
|
|
122
92
|
// When searchMemories is called with recordRecall=true it receives the
|
|
123
93
|
// actual merged recall scene (candidates + scores + source + threshold) so
|
|
@@ -258,36 +228,6 @@ export function createService({ store, mirror, config, onWrite, logger, settings
|
|
|
258
228
|
}
|
|
259
229
|
}
|
|
260
230
|
|
|
261
|
-
/**
|
|
262
|
-
* Fire-and-forget wiki-link resolution for a freshly saved/updated memory
|
|
263
|
-
* (v0.6.1). Opt-in via config.wikiLinkEnabled. Parses [[target]] /
|
|
264
|
-
* [[显示|target]] markers out of the memory content and writes links_to
|
|
265
|
-
* relations (idempotent via the unique relation index). Runs through
|
|
266
|
-
* service.enqueue so it serializes with autoDream/sleep and never overlaps
|
|
267
|
-
* another background pass. Fully fail-safe: parse/store errors are swallowed
|
|
268
|
-
* and logged, never a write failure.
|
|
269
|
-
*/
|
|
270
|
-
function scheduleWikiLinkResolve(memory) {
|
|
271
|
-
if (txDepth > 0) return; // deferred to the transaction's commit
|
|
272
|
-
if (!config.wikiLinkEnabled || !memory?.id) return;
|
|
273
|
-
try {
|
|
274
|
-
const links = parseWikiLinks(memory?.content ?? "");
|
|
275
|
-
if (!links.length) return;
|
|
276
|
-
const targets = [...new Set(links.map((l) => l.target).filter(Boolean))];
|
|
277
|
-
enqueue(() => {
|
|
278
|
-
try {
|
|
279
|
-
store.saveWikiLinks({ memoryId: memory.id, title: memory.title, targets });
|
|
280
|
-
} catch (err) {
|
|
281
|
-
logger?.warn?.("wiki link resolve failed:", err);
|
|
282
|
-
}
|
|
283
|
-
}).catch((err) => {
|
|
284
|
-
logger?.warn?.("wiki link resolve failed:", err);
|
|
285
|
-
});
|
|
286
|
-
} catch (err) {
|
|
287
|
-
logger?.warn?.("wiki link resolve failed:", err);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
231
|
/**
|
|
292
232
|
* Cross-encoder rerank over a candidate list (best effort). Reranker
|
|
293
233
|
* failures degrade to the original candidate order — reranking is an
|
|
@@ -356,57 +296,6 @@ export function createService({ store, mirror, config, onWrite, logger, settings
|
|
|
356
296
|
return hits;
|
|
357
297
|
}
|
|
358
298
|
|
|
359
|
-
/**
|
|
360
|
-
* Search for memories carrying a given tag set (v0.6.2). Multiple tag:
|
|
361
|
-
* tokens in one query use AND semantics — a memory must carry every tag.
|
|
362
|
-
* Combinable with the leftover query: an entity:/attr: prefix is intersected
|
|
363
|
-
* with the tag-matched set, and plain keyword text ranks it (only rows whose
|
|
364
|
-
* keyword score > 0 survive). Tags are first-class recall, not entity-gated:
|
|
365
|
-
* the tag match itself does not depend on config.entitySearchEnabled.
|
|
366
|
-
* @param {string[]} tagTokens
|
|
367
|
-
* @param {string} q — leftover query after tag: tokens were stripped
|
|
368
|
-
* @param {object} [options]
|
|
369
|
-
* @param {number} [options.topK=20]
|
|
370
|
-
* @returns {any[]}
|
|
371
|
-
*/
|
|
372
|
-
function searchByTags(tagTokens, q, options) {
|
|
373
|
-
const { topK = 20 } = options;
|
|
374
|
-
const tags = (Array.isArray(tagTokens) ? tagTokens : [])
|
|
375
|
-
.map((t) => String(t).trim()).filter(Boolean);
|
|
376
|
-
if (!tags.length) return [];
|
|
377
|
-
let hits = store.findMemoriesByTags(tags);
|
|
378
|
-
if (!hits.length) return [];
|
|
379
|
-
// Intersect with entity:/attr: prefixes present in the leftover query.
|
|
380
|
-
if (config?.entitySearchEnabled) {
|
|
381
|
-
if (q.startsWith("entity:")) {
|
|
382
|
-
const ids = new Set(searchByEntity(q.slice(7).trim(), { topK: 10000 }).map((m) => m.id));
|
|
383
|
-
hits = hits.filter((m) => ids.has(m.id));
|
|
384
|
-
}
|
|
385
|
-
if (q.startsWith("attr:")) {
|
|
386
|
-
const [key, value] = q.slice(5).split("=");
|
|
387
|
-
const ids = new Set(searchByAttr(key, value, { topK: 10000 }).map((m) => m.id));
|
|
388
|
-
hits = hits.filter((m) => ids.has(m.id));
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
// Leftover plain text ranks the tag-matched set; only rows whose title or
|
|
392
|
-
// content actually mentions the keyword survive (scoreKeyword's 0.3 base is
|
|
393
|
-
// a non-match, so the containment check is the real gate); otherwise
|
|
394
|
-
// preserve insertion order.
|
|
395
|
-
const keywordOnly = q && !q.startsWith("entity:") && !q.startsWith("attr:");
|
|
396
|
-
hits = keywordOnly
|
|
397
|
-
? hits
|
|
398
|
-
.map((m) => ({ ...m, score: scoreKeyword(m, q), source: "tag" }))
|
|
399
|
-
.filter((m) => (m.title ?? "").toLowerCase().includes(q.toLowerCase())
|
|
400
|
-
|| (m.content ?? "").toLowerCase().includes(q.toLowerCase()))
|
|
401
|
-
.sort((a, b) => b.score - a.score)
|
|
402
|
-
: hits.map((m) => ({ ...m, source: "tag" }));
|
|
403
|
-
const result = hits.slice(0, topK);
|
|
404
|
-
// Only count toward recall stats/forgetting when the caller asked for
|
|
405
|
-
// recording — the panel's `tag:` search must not pollute the curve.
|
|
406
|
-
if (options.recordRecall) touchRecalled(result);
|
|
407
|
-
return result;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
299
|
/**
|
|
411
300
|
* Semantic-aware memory search: keyword recall (store.search) plus optional
|
|
412
301
|
* vector recall + rerank. mode:
|
|
@@ -442,7 +331,7 @@ export function createService({ store, mirror, config, onWrite, logger, settings
|
|
|
442
331
|
function bm25Recall(q, limit) {
|
|
443
332
|
if (config?.bm25SearchEnabled === false) return [];
|
|
444
333
|
try {
|
|
445
|
-
const docs = store.list({ limit: 500, includeForgotten: false }).filter((m) => !m.archived
|
|
334
|
+
const docs = store.list({ limit: 500, includeForgotten: false }).filter((m) => !m.archived);
|
|
446
335
|
if (!docs.length) return [];
|
|
447
336
|
return createBM25Index(docs).search(q, { limit });
|
|
448
337
|
} catch {
|
|
@@ -495,15 +384,14 @@ export function createService({ store, mirror, config, onWrite, logger, settings
|
|
|
495
384
|
}
|
|
496
385
|
|
|
497
386
|
/**
|
|
498
|
-
*
|
|
499
|
-
* auto-injection gets its last_accessed_at bumped so the
|
|
500
|
-
*
|
|
501
|
-
*
|
|
502
|
-
*
|
|
503
|
-
* a touch failure must never break search/inject.
|
|
387
|
+
* Sleep touch (v0.4.0): when sleep is enabled, any memory surfaced by recall
|
|
388
|
+
* or auto-injection gets its last_accessed_at bumped, so the "unrecalled N
|
|
389
|
+
* days → demote/archive" tiering counts real access. Best-effort and gated on
|
|
390
|
+
* config.sleepModeEnabled — when sleep is off this is a complete no-op (no
|
|
391
|
+
* writes on the hot recall path). A touch failure must never break search/inject.
|
|
504
392
|
*/
|
|
505
393
|
function touchRecalled(memories) {
|
|
506
|
-
if (config?.
|
|
394
|
+
if (config?.sleepModeEnabled !== true || !Array.isArray(memories) || memories.length === 0) return;
|
|
507
395
|
for (const m of memories) {
|
|
508
396
|
if (!m?.id) continue;
|
|
509
397
|
try {
|
|
@@ -513,27 +401,9 @@ export function createService({ store, mirror, config, onWrite, logger, settings
|
|
|
513
401
|
}
|
|
514
402
|
|
|
515
403
|
async function searchMemories(query, options = {}) {
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
mode = "auto",
|
|
520
|
-
topK = 20,
|
|
521
|
-
threshold,
|
|
522
|
-
useRerank = true,
|
|
523
|
-
recordRecall = options.recordRecall ?? (config?.recallRecordDefault ?? true)
|
|
524
|
-
} = options;
|
|
525
|
-
const raw = String(query ?? "").trim();
|
|
526
|
-
if (!raw) return [];
|
|
527
|
-
|
|
528
|
-
// tag: 前缀(v0.6.2)。先把所有 tag: 令牌从 query 里剥出来,剩余的 q 仍可带
|
|
529
|
-
// entity:/attr: 前缀或普通关键词 —— searchByTags 负责交集/排序。没有 tag:
|
|
530
|
-
// 令牌则走下面的 entity:/attr:/文本原逻辑(完全向后兼容)。
|
|
531
|
-
const tagTokens = [];
|
|
532
|
-
const q = raw.replace(/\btag:([^\s]+)/g, (_, tok) => {
|
|
533
|
-
if (tok) tagTokens.push(tok);
|
|
534
|
-
return "";
|
|
535
|
-
}).trim();
|
|
536
|
-
if (tagTokens.length) return searchByTags(tagTokens, q, options);
|
|
404
|
+
const { mode = "auto", topK = 20, threshold, useRerank = true, recordRecall = false } = options;
|
|
405
|
+
const q = String(query ?? "").trim();
|
|
406
|
+
if (!q) return [];
|
|
537
407
|
|
|
538
408
|
// entity:/attr: 前缀路由(v0.3.0 Phase 3)。entitySearchEnabled 关闭时走原逻辑。
|
|
539
409
|
if (config?.entitySearchEnabled) {
|
|
@@ -635,13 +505,8 @@ export function createService({ store, mirror, config, onWrite, logger, settings
|
|
|
635
505
|
byId.set(m.id, { ...m, score: wb * (m.score ?? 0) });
|
|
636
506
|
}
|
|
637
507
|
}
|
|
638
|
-
const ranked = [...byId.values()]
|
|
639
|
-
|
|
640
|
-
.map((r) => ({ ...r, score: Math.max(0, Math.min(1, r.score ?? 0)) }));
|
|
641
|
-
// Tag boost (v0.6.4) needs the full ranked pool, not just the top-lim
|
|
642
|
-
// slice, so a tagged candidate just below the line can be re-admitted
|
|
643
|
-
// after the boost. Without boost this is the legacy lim truncation.
|
|
644
|
-
merged = config.tagBoostEnabled === true ? ranked : ranked.slice(0, lim);
|
|
508
|
+
const ranked = [...byId.values()].sort((a, b) => (b.score ?? 0) - (a.score ?? 0));
|
|
509
|
+
merged = ranked.slice(0, lim);
|
|
645
510
|
if (merged.length < lim && !merged.length) {
|
|
646
511
|
// Vector unavailable entirely: fall back to plain keyword.
|
|
647
512
|
merged = keyword.slice(0, lim);
|
|
@@ -667,31 +532,7 @@ export function createService({ store, mirror, config, onWrite, logger, settings
|
|
|
667
532
|
// it is the documented text-only path and must not be altered by
|
|
668
533
|
// embedding state.
|
|
669
534
|
merged = mode === "keyword" ? merged : semanticDeduplicate(merged);
|
|
670
|
-
|
|
671
|
-
// Tag-weighted re-rank (v0.6.4): boost candidates whose tags overlap the
|
|
672
|
-
// query tags or the current session's hot-memory tags — applied BEFORE the
|
|
673
|
-
// final top-K cut so tagged candidates just below the line can be
|
|
674
|
-
// re-admitted. Opt-in, and skipped entirely on the keyword-only path. When
|
|
675
|
-
// a reranker is configured (opt-in), it remains the final authority on
|
|
676
|
-
// order; the boost still shapes which candidates reach it.
|
|
677
|
-
if (config.tagBoostEnabled === true && mode !== "keyword" && merged.length) {
|
|
678
|
-
const ids = merged.map((m) => m.id);
|
|
679
|
-
const tagsMap = store.getMemoryTagsMap(ids);
|
|
680
|
-
const enriched = merged.map((m) => ({ ...m, tags: tagsMap.get(m.id) ?? [] }));
|
|
681
|
-
const knownTags = [...tagsMap.values()].flat();
|
|
682
|
-
const queryTags = extractQueryTags(q, knownTags);
|
|
683
|
-
const sessionTags = Array.isArray(options?.sessionTags) ? options.sessionTags : [];
|
|
684
|
-
if (queryTags.length || sessionTags.length) {
|
|
685
|
-
merged = applyTagBoost(enriched, {
|
|
686
|
-
queryTags,
|
|
687
|
-
sessionTags,
|
|
688
|
-
factor: config.tagBoostFactor,
|
|
689
|
-
sessionFactor: config.sessionTagBoostFactor,
|
|
690
|
-
}).map(({ tags, ...rest }) => rest);
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
535
|
merged = merged.slice(0, lim);
|
|
694
|
-
|
|
695
536
|
let result = useRerank && reranker && merged.length
|
|
696
537
|
? await rerankCandidates(q, merged, lim)
|
|
697
538
|
: merged;
|
|
@@ -720,16 +561,12 @@ export function createService({ store, mirror, config, onWrite, logger, settings
|
|
|
720
561
|
mode,
|
|
721
562
|
topK: lim,
|
|
722
563
|
threshold: threshold ?? null,
|
|
723
|
-
// v0.7.0 recall-layer 强度标记:搜索帧一律 injected:false(被召回 =
|
|
724
|
-
// 检索直接命中);注入上下文帧由 injectCandidates 以 injected:true
|
|
725
|
-
// 单独记账(被注入 = 替对话搭建上下文),两档强度可区分。
|
|
726
564
|
candidates: result.map((m) => ({
|
|
727
565
|
id: m.id,
|
|
728
566
|
title: m.title,
|
|
729
567
|
content: m.content,
|
|
730
568
|
score: m.score ?? null,
|
|
731
|
-
source: m.source ?? "keyword"
|
|
732
|
-
injected: false
|
|
569
|
+
source: m.source ?? "keyword"
|
|
733
570
|
})),
|
|
734
571
|
createdAt: new Date().toISOString()
|
|
735
572
|
});
|
|
@@ -960,17 +797,9 @@ export function createService({ store, mirror, config, onWrite, logger, settings
|
|
|
960
797
|
});
|
|
961
798
|
// Bug7: a degraded/archived result is applied on top of the merged row.
|
|
962
799
|
const result = applyQualityDisposition(merged, quality, qf);
|
|
963
|
-
// Bridge tool-passed tags (plus any quality signal tags) into the
|
|
964
|
-
// entity_attrs tag store so directory / tag: recall / tagBoost see them
|
|
965
|
-
// (issue #31). Must run before afterSync so the mirror re-render reads
|
|
966
|
-
// the bridged row. Unconditional on the array (empty included): a merge
|
|
967
|
-
// that clears tags must also invalidate any stale entity_attrs row from
|
|
968
|
-
// an earlier save, or the directory keeps showing the old tag.
|
|
969
|
-
if (Array.isArray(result.tags)) store.setMemoryTags(result.id, result.tags);
|
|
970
800
|
afterSync("write");
|
|
971
801
|
notifyWrite();
|
|
972
802
|
scheduleEmbed(result);
|
|
973
|
-
scheduleWikiLinkResolve(result);
|
|
974
803
|
return { action: "merged", memory: result };
|
|
975
804
|
}
|
|
976
805
|
const created = store.save({
|
|
@@ -980,22 +809,13 @@ export function createService({ store, mirror, config, onWrite, logger, settings
|
|
|
980
809
|
tags: memory.tags ?? [],
|
|
981
810
|
importance: memory.importance ?? 3,
|
|
982
811
|
source: memory.source ?? "manual",
|
|
983
|
-
// Provenance (v0.5.x): birth session rides through the create path; the
|
|
984
|
-
// merge path above preserves the original row's session_id untouched.
|
|
985
|
-
session_id: memory.session_id ?? undefined,
|
|
986
812
|
...(quality ? { quality_score: quality.score } : {})
|
|
987
813
|
});
|
|
988
814
|
const result = applyQualityDisposition(created, quality, qf);
|
|
989
|
-
// Bridge tool-passed tags (plus any quality signal tags) into the
|
|
990
|
-
// entity_attrs tag store so directory / tag: recall / tagBoost see them
|
|
991
|
-
// (issue #31). Must run before afterSync so the mirror re-render reads
|
|
992
|
-
// the bridged row.
|
|
993
|
-
if (Array.isArray(result.tags) && result.tags.length) store.setMemoryTags(result.id, result.tags);
|
|
994
815
|
afterSync("write");
|
|
995
816
|
notifyWrite();
|
|
996
817
|
scheduleEmbed(result);
|
|
997
818
|
scheduleEntityExtraction(result);
|
|
998
|
-
scheduleWikiLinkResolve(result);
|
|
999
819
|
return { action: "created", memory: result };
|
|
1000
820
|
}
|
|
1001
821
|
|
|
@@ -1047,15 +867,12 @@ export function createService({ store, mirror, config, onWrite, logger, settings
|
|
|
1047
867
|
// (quality_score null) count as 100 (weight 1), so legacy stores keep their
|
|
1048
868
|
// exact summary>preference>importance ordering.
|
|
1049
869
|
const qualityWeight = (m) => (m.quality_score != null ? m.quality_score / 100 : 1);
|
|
1050
|
-
// summary/user/preference are injected regardless of importance (context
|
|
1051
|
-
// layers); every other INJECT_TYPES type (e.g. fact) only when
|
|
1052
|
-
// importance >= threshold — the frontend "常注入" badge maps to this split.
|
|
1053
870
|
const items = store.list({ limit: 200, includeForgotten: false })
|
|
1054
871
|
.filter((m) => !m.archived && INJECT_TYPES.has(m.type) && !m.forgotten &&
|
|
1055
|
-
(m.type === "summary" || m.type === "preference" || m.
|
|
872
|
+
(m.type === "summary" || m.type === "preference" || m.importance >= threshold))
|
|
1056
873
|
.sort((a, b) => {
|
|
1057
|
-
const pa = a.type === "summary" ? 0 :
|
|
1058
|
-
const pb = b.type === "summary" ? 0 :
|
|
874
|
+
const pa = a.type === "summary" ? 0 : a.type === "preference" ? 1 : 2;
|
|
875
|
+
const pb = b.type === "summary" ? 0 : b.type === "preference" ? 1 : 2;
|
|
1059
876
|
return pa - pb || (b.importance * qualityWeight(b)) - (a.importance * qualityWeight(a));
|
|
1060
877
|
});
|
|
1061
878
|
let candidates = items;
|
|
@@ -1071,7 +888,7 @@ export function createService({ store, mirror, config, onWrite, logger, settings
|
|
|
1071
888
|
const hits = vectorIndex.search(queryVector, { limit: maxItems * 2, threshold: 0 });
|
|
1072
889
|
for (const m of hits) {
|
|
1073
890
|
if (m && !m.archived && INJECT_TYPES.has(m.type) && !m.forgotten &&
|
|
1074
|
-
(m.type === "summary" || m.type === "preference" || m.
|
|
891
|
+
(m.type === "summary" || m.type === "preference" || m.importance >= threshold)) {
|
|
1075
892
|
semanticItems.push(m);
|
|
1076
893
|
}
|
|
1077
894
|
}
|
|
@@ -1116,32 +933,6 @@ export function createService({ store, mirror, config, onWrite, logger, settings
|
|
|
1116
933
|
} catch { /* topic re-rank unavailable: keep rule-based order */ }
|
|
1117
934
|
}
|
|
1118
935
|
const selected = candidates.slice(0, maxItems);
|
|
1119
|
-
// v0.7.0 recall-layer 强度标记:注入场景也走 recall_runs 记账(mode="inject"、
|
|
1120
|
-
// injected:true),与 searchMemories 的搜索帧(injected:false)互补,区分
|
|
1121
|
-
// "被召回"与"被注入上下文"两个消耗强度。与搜索侧同受 recallRecordDefault
|
|
1122
|
-
// 门控;记账失败绝不阻断注入本身。
|
|
1123
|
-
if (recallRecorder && config.recallRecordDefault !== false) {
|
|
1124
|
-
try {
|
|
1125
|
-
recallRecorder({
|
|
1126
|
-
query: q,
|
|
1127
|
-
mode: "inject",
|
|
1128
|
-
topK: selected.length,
|
|
1129
|
-
threshold: null,
|
|
1130
|
-
candidates: selected.map((m) => ({
|
|
1131
|
-
id: m.id,
|
|
1132
|
-
title: m.title,
|
|
1133
|
-
content: m.content,
|
|
1134
|
-
score: m.score ?? null,
|
|
1135
|
-
// 注入帧的 source 固定为帧级标记 "inject",与搜索帧的
|
|
1136
|
-
// keyword/vector/rerank 对齐(记忆行自带的 source 是来源列,
|
|
1137
|
-
// 与检索层级无关,不混入)。
|
|
1138
|
-
source: "inject",
|
|
1139
|
-
injected: true
|
|
1140
|
-
})),
|
|
1141
|
-
createdAt: new Date().toISOString()
|
|
1142
|
-
});
|
|
1143
|
-
} catch { /* recall receipt is best effort */ }
|
|
1144
|
-
}
|
|
1145
936
|
touchRecalled(selected);
|
|
1146
937
|
return selected;
|
|
1147
938
|
}
|
|
@@ -1203,32 +994,11 @@ export function createService({ store, mirror, config, onWrite, logger, settings
|
|
|
1203
994
|
tags: m.tags,
|
|
1204
995
|
importance: m.importance,
|
|
1205
996
|
source: m.source,
|
|
1206
|
-
// session_id is optional on the wire: only carry it when present, so the
|
|
1207
|
-
// DTO stays a lossless JSON object (undefined would vanish on serialize).
|
|
1208
|
-
...(m.session_id != null ? { session_id: m.session_id } : {}),
|
|
1209
|
-
// Disposed state rides along when set, so a restore flow is not a blind
|
|
1210
|
-
// op — the caller can see which entries are hidden before restoreBySession.
|
|
1211
|
-
...(m.session_disposed_at != null ? { disposed: true } : {}),
|
|
1212
997
|
created_at: m.created_at,
|
|
1213
998
|
updated_at: m.updated_at
|
|
1214
999
|
}));
|
|
1215
1000
|
}
|
|
1216
1001
|
|
|
1217
|
-
/**
|
|
1218
|
-
* Directory view (v0.6.3): group live memories by tag. Delegates to
|
|
1219
|
-
* store.getDirectory (tag-sorted groups, importance/updated DESC members,
|
|
1220
|
-
* live-only filtering) and maps every memory to the wire DTO so the result
|
|
1221
|
-
* is JSON-safe for the API endpoint.
|
|
1222
|
-
* @returns {{groups: {tag: string, memories: object[]}[], untagged: object[]}}
|
|
1223
|
-
*/
|
|
1224
|
-
function getDirectory() {
|
|
1225
|
-
const { groups, untagged } = store.getDirectory();
|
|
1226
|
-
return {
|
|
1227
|
-
groups: groups.map((g) => ({ tag: g.tag, memories: toApiList(g.memories) })),
|
|
1228
|
-
untagged: toApiList(untagged)
|
|
1229
|
-
};
|
|
1230
|
-
}
|
|
1231
|
-
|
|
1232
1002
|
/**
|
|
1233
1003
|
* Three-way merge of in-flight human mirror edits before a re-render.
|
|
1234
1004
|
* Runs on every syncMirror, so a human edit made between two store writes is
|
|
@@ -1336,14 +1106,8 @@ export function createService({ store, mirror, config, onWrite, logger, settings
|
|
|
1336
1106
|
// leaving committed files mislabeled as failed and masking partial state.
|
|
1337
1107
|
// Absent entries (a type with no memories) count as success: sync prunes
|
|
1338
1108
|
// the stale file, which is itself a completed physical state.
|
|
1339
|
-
// v0.6.2: attach entity_attrs-backed tags (entityTags) after the human-edit
|
|
1340
|
-
// merge so renderMemory can draw the `#tag` line under each title. The
|
|
1341
|
-
// bulk map is a single query, and a missing row simply renders no line.
|
|
1342
|
-
const reconciled = reconcileHumanEdits(list);
|
|
1343
|
-
const tagsMap = store.getMemoryTagsMap(reconciled.map((m) => m.id));
|
|
1344
|
-
const tagged = reconciled.map((m) => ({ ...m, entityTags: tagsMap.get(m.id) ?? [] }));
|
|
1345
1109
|
let allOk = true;
|
|
1346
|
-
const results = mirror.sync(
|
|
1110
|
+
const results = mirror.sync(reconcileHumanEdits(list)) ?? {};
|
|
1347
1111
|
for (const type of Object.keys(TYPE_FILE)) {
|
|
1348
1112
|
const r = results[type];
|
|
1349
1113
|
const ok = !r || r.ok === true;
|
|
@@ -1511,59 +1275,14 @@ export function createService({ store, mirror, config, onWrite, logger, settings
|
|
|
1511
1275
|
}
|
|
1512
1276
|
}
|
|
1513
1277
|
|
|
1514
|
-
/**
|
|
1515
|
-
* Forward links (v0.6.1): memories the given memory explicitly links to via
|
|
1516
|
-
* [[wiki-links]] in its content. Reads the links_to relations whose
|
|
1517
|
-
* from_entity is this memory's title and resolves each to_entity back to a
|
|
1518
|
-
* memory row (case-insensitive title match). Returns [{ target, relation }];
|
|
1519
|
-
* a target title with no matching memory surfaces as { target: null }.
|
|
1520
|
-
*/
|
|
1521
|
-
function getForwardLinks(memoryId) {
|
|
1522
|
-
const memory = store.getById(memoryId);
|
|
1523
|
-
if (!memory) return [];
|
|
1524
|
-
const out = [];
|
|
1525
|
-
for (const rel of store.getRelations(memory.title) ?? []) {
|
|
1526
|
-
if (rel.relation_type !== "links_to" || rel.from_entity !== memory.title) continue;
|
|
1527
|
-
out.push({ target: store.findByTitle?.(rel.to_entity) ?? null, relation: rel });
|
|
1528
|
-
}
|
|
1529
|
-
return out;
|
|
1530
|
-
}
|
|
1531
|
-
|
|
1532
|
-
/**
|
|
1533
|
-
* Back links (v0.6.1): memories that explicitly link TO the given memory
|
|
1534
|
-
* (their content carries a wiki-link whose target resolves to this memory's
|
|
1535
|
-
* title). Reads the links_to relations whose to_entity is this memory's
|
|
1536
|
-
* title; the linking memory is rel.memory_id (the source that wrote the
|
|
1537
|
-
* relation). Deduped per source memory; missing/self links are dropped.
|
|
1538
|
-
* Returns [{ source, relation }].
|
|
1539
|
-
*/
|
|
1540
|
-
function getBacklinks(memoryId) {
|
|
1541
|
-
const memory = store.getById(memoryId);
|
|
1542
|
-
if (!memory) return [];
|
|
1543
|
-
const out = [];
|
|
1544
|
-
const seen = new Set();
|
|
1545
|
-
for (const rel of store.getRelations(memory.title) ?? []) {
|
|
1546
|
-
if (rel.relation_type !== "links_to" || rel.to_entity !== memory.title) continue;
|
|
1547
|
-
const source = rel.memory_id ? store.getById(rel.memory_id) : undefined;
|
|
1548
|
-
if (!source || source.id === memory.id || seen.has(source.id)) continue;
|
|
1549
|
-
seen.add(source.id);
|
|
1550
|
-
out.push({ source, relation: rel });
|
|
1551
|
-
}
|
|
1552
|
-
return out;
|
|
1553
|
-
}
|
|
1554
|
-
|
|
1555
1278
|
return {
|
|
1556
1279
|
saveWithDedupe,
|
|
1557
|
-
getBacklinks,
|
|
1558
|
-
getForwardLinks,
|
|
1559
|
-
resolveWikiLink: (title) => store.findByTitle?.(title),
|
|
1560
1280
|
recoverMirror,
|
|
1561
1281
|
getMirrorHealth,
|
|
1562
1282
|
getMirrorState: () => store.getMirrorState(),
|
|
1563
1283
|
injectCandidates,
|
|
1564
1284
|
mergeHumanEdits,
|
|
1565
1285
|
toApiList,
|
|
1566
|
-
getDirectory,
|
|
1567
1286
|
transaction,
|
|
1568
1287
|
enqueue,
|
|
1569
1288
|
setDreamHook(fn) { dreamHook = fn; },
|
|
@@ -1608,83 +1327,16 @@ export function createService({ store, mirror, config, onWrite, logger, settings
|
|
|
1608
1327
|
embeddedCount: () => store.embeddedCount(),
|
|
1609
1328
|
list: (o) => store.list(o),
|
|
1610
1329
|
all: () => store.all(),
|
|
1611
|
-
listForEntityExtraction: (opts) => store.listForEntityExtraction(opts),
|
|
1612
1330
|
count: (type, opts) => store.count(type, opts),
|
|
1613
|
-
stats: (opts) => store.stats(opts),
|
|
1614
1331
|
getById: (id) => store.getById(id),
|
|
1615
|
-
// issue #48: resolve a possibly-truncated id to its canonical full id.
|
|
1616
|
-
// Exact hit wins; otherwise the input is treated as a prefix of the id
|
|
1617
|
-
// PRIMARY KEY. Never guesses on ambiguity — returns the candidates and the
|
|
1618
|
-
// caller must pass a full id. Outcome is {ok:true,id} or {ok:false,reason,
|
|
1619
|
-
// message} with reason ∈ invalid | not-found | ambiguous. warnMiss logs the
|
|
1620
|
-
// silent-miss case the delete tool previously swallowed (no return channel
|
|
1621
|
-
// for it, so observability has to live here in the service layer).
|
|
1622
|
-
resolveMemoryId: (input, { warnMiss = false } = {}) => {
|
|
1623
|
-
const bad = (reason, message) => ({ ok: false, reason, message });
|
|
1624
|
-
if (typeof input !== "string") return bad("invalid", "memory id is required");
|
|
1625
|
-
// 手抄/上下文压缩来的 id 可能带首尾空白,统一 trim 后再做精确与前缀解析。
|
|
1626
|
-
const id = input.trim();
|
|
1627
|
-
if (!id) return bad("invalid", "memory id is required");
|
|
1628
|
-
const exact = store.getById(id);
|
|
1629
|
-
if (exact) return { ok: true, id: exact.id };
|
|
1630
|
-
const matches = store.listByIdPrefix(id);
|
|
1631
|
-
if (matches.length === 0) {
|
|
1632
|
-
if (warnMiss) {
|
|
1633
|
-
logger?.warn?.(
|
|
1634
|
-
`[dsh-mneme] memory id "${id}" matched nothing (exact or prefix) — ` +
|
|
1635
|
-
"no entry was deleted; ids are full-length, pass one from memory_list/memory_search output or delete by query=…"
|
|
1636
|
-
);
|
|
1637
|
-
}
|
|
1638
|
-
return bad(
|
|
1639
|
-
"not-found",
|
|
1640
|
-
`memory not found: ${id} (checked exact id and prefix; use a full id from memory_list/memory_search output)`
|
|
1641
|
-
);
|
|
1642
|
-
}
|
|
1643
|
-
if (matches.length > 1) {
|
|
1644
|
-
const sample = matches.slice(0, 5).map((m) => m.id);
|
|
1645
|
-
const tail = matches.length > sample.length ? ` …(+${matches.length - sample.length})` : "";
|
|
1646
|
-
return bad(
|
|
1647
|
-
"ambiguous",
|
|
1648
|
-
`memory id prefix "${id}" matches ${matches.length} entries (${sample.join(", ")}${tail}); refusing to guess — pass a full id`
|
|
1649
|
-
);
|
|
1650
|
-
}
|
|
1651
|
-
return { ok: true, id: matches[0].id };
|
|
1652
|
-
},
|
|
1653
1332
|
remove: (id) => {
|
|
1654
1333
|
store.remove(id);
|
|
1655
1334
|
afterSync("write");
|
|
1656
1335
|
notifyWrite();
|
|
1657
1336
|
},
|
|
1658
|
-
// Session lifecycle (v0.6.0): mark/clear the session-disposed state on every
|
|
1659
|
-
// memory born in a given session. Uses the dedicated `session_disposed_at`
|
|
1660
|
-
// column, orthogonal to `archived` — restoring a session never resurrects
|
|
1661
|
-
// memories the user archived on purpose. Nothing is destroyed; a session
|
|
1662
|
-
// treated as a save point is fully recoverable via restoreBySession.
|
|
1663
|
-
disposeBySession: (sessionId) => {
|
|
1664
|
-
const disposed = store.setDisposedBySession(sessionId, true);
|
|
1665
|
-
if (disposed > 0) {
|
|
1666
|
-
afterSync("write");
|
|
1667
|
-
notifyWrite();
|
|
1668
|
-
}
|
|
1669
|
-
return { disposed };
|
|
1670
|
-
},
|
|
1671
|
-
restoreBySession: (sessionId) => {
|
|
1672
|
-
const restored = store.setDisposedBySession(sessionId, false);
|
|
1673
|
-
if (restored > 0) {
|
|
1674
|
-
afterSync("write");
|
|
1675
|
-
notifyWrite();
|
|
1676
|
-
}
|
|
1677
|
-
return { restored };
|
|
1678
|
-
},
|
|
1679
|
-
listBySession: (sessionId, opts = {}) => toApiList(store.listBySession(sessionId, opts)),
|
|
1680
1337
|
update: (id, p, ctx = {}) => {
|
|
1681
1338
|
const old = store.getById(id);
|
|
1682
1339
|
const updated = store.update(id, p);
|
|
1683
|
-
// Bridge tag edits made through update (memory_update tool / panel) into
|
|
1684
|
-
// the entity_attrs tag store too (issue #31) — same convergence as
|
|
1685
|
-
// saveWithDedupe. An explicit empty array clears the live tag row, moving
|
|
1686
|
-
// the memory back to `untagged`.
|
|
1687
|
-
if (p?.tags !== undefined) store.setMemoryTags(updated.id, updated.tags);
|
|
1688
1340
|
// Record a user correction when any meaningful field changed and the
|
|
1689
1341
|
// reflection failure tracker is enabled. expected = what it became,
|
|
1690
1342
|
// actual = what it was before; query (when provided) captures the
|
|
@@ -1708,7 +1360,6 @@ export function createService({ store, mirror, config, onWrite, logger, settings
|
|
|
1708
1360
|
const sync = afterSync("write");
|
|
1709
1361
|
notifyWrite();
|
|
1710
1362
|
scheduleEmbed(updated);
|
|
1711
|
-
scheduleWikiLinkResolve(updated);
|
|
1712
1363
|
// Audit peer B: when the mirror sync failed, the store write landed but
|
|
1713
1364
|
// the mirror did not converge — return an explicit degraded receipt rather
|
|
1714
1365
|
// than a plain success. Non-enumerable so existing deepEqual assertions on
|
|
@@ -1820,54 +1471,13 @@ export function createService({ store, mirror, config, onWrite, logger, settings
|
|
|
1820
1471
|
// migrates entity_attrs on merge. Bookkeeping writes like the audit
|
|
1821
1472
|
// passthroughs above — never write-hook-triggering memory mutations.
|
|
1822
1473
|
saveRelation: (r) => store.saveRelation(r),
|
|
1823
|
-
saveWikiLinks: (r) => store.saveWikiLinks(r),
|
|
1824
1474
|
listEntities: (o) => store.listEntities(o),
|
|
1825
1475
|
getRelations: (id) => store.getRelations(id),
|
|
1826
|
-
// v0.7.0 实体热投影:实体热 = 关联记忆 heat 聚合(取 max)。无关联记忆
|
|
1827
|
-
// 或 heatEnabled=false 时返回 null;前端据此决定图谱节点大小/明暗。
|
|
1828
|
-
entityHeat: (entityId) => {
|
|
1829
|
-
if (config.heatEnabled === false) return null;
|
|
1830
|
-
const rels = store.getRelations(entityId) ?? [];
|
|
1831
|
-
let max = -Infinity;
|
|
1832
|
-
for (const rel of rels) {
|
|
1833
|
-
if (!rel.memory_id) continue;
|
|
1834
|
-
const mem = store.getById(rel.memory_id);
|
|
1835
|
-
if (!mem) continue;
|
|
1836
|
-
const h = computeHeat(mem, Date.now(), config);
|
|
1837
|
-
if (h > max) max = h;
|
|
1838
|
-
}
|
|
1839
|
-
return max === -Infinity ? null : max;
|
|
1840
|
-
},
|
|
1841
|
-
// Tag system (v0.6.2). setMemoryTags is the manual/user path: gated by
|
|
1842
|
-
// config.manualTagEnabled (default true), re-renders the mirror and fires
|
|
1843
|
-
// the write hook. applyMemoryTags is the raw write used by the autoDream
|
|
1844
|
-
// tag pass (gated by autoTagEnabled, wrapped in a transaction by the
|
|
1845
|
-
// extractor so the mirror re-renders exactly once).
|
|
1846
|
-
setMemoryTags: (memoryId, tags) => {
|
|
1847
|
-
if (tagConfig().manualTagEnabled === false) {
|
|
1848
|
-
return { ok: false, error: "manualTagEnabled is off" };
|
|
1849
|
-
}
|
|
1850
|
-
const stored = store.setMemoryTags(memoryId, tags);
|
|
1851
|
-
afterSync("write");
|
|
1852
|
-
notifyWrite();
|
|
1853
|
-
return { ok: true, tags: stored };
|
|
1854
|
-
},
|
|
1855
|
-
getMemoryTags: (memoryId) => store.getMemoryTags(memoryId),
|
|
1856
|
-
// Read-only gate flag so the Web panel can hide tag editing when the
|
|
1857
|
-
// manual path is disabled (default: manual tagging is on).
|
|
1858
|
-
manualTagEnabled: () => tagConfig().manualTagEnabled !== false,
|
|
1859
|
-
// Effective tag toggle pair (settings-over-config merge) for the panel
|
|
1860
|
-
// (/api/dsh-mneme/config GET/PUT).
|
|
1861
|
-
getTagConfig: tagConfig,
|
|
1862
|
-
getAppConfig: appConfig,
|
|
1863
|
-
applyMemoryTags: (memoryId, tags) => store.setMemoryTags(memoryId, tags),
|
|
1864
1476
|
saveAttr: (r) => store.saveAttr(r),
|
|
1865
1477
|
createEntity: (r) => store.createEntity(r),
|
|
1866
|
-
updateEntity: (id, patch) => store.updateEntity(id, patch),
|
|
1867
1478
|
findEntityByName: (n) => store.findEntityByName(n),
|
|
1868
1479
|
findEntityById: (id) => store.findEntityById(id),
|
|
1869
1480
|
getAttrsByMemory: (id) => store.getAttrsByMemory(id),
|
|
1870
|
-
setMemoryMetadata: (id, metadata) => store.setMemoryMetadata(id, metadata),
|
|
1871
1481
|
getCurrentAttrs: (id) => store.getCurrentAttrs(id),
|
|
1872
1482
|
migrateAttrsToMemory: (fromId, toId, now) => store.migrateAttrsToMemory(fromId, toId, now)
|
|
1873
1483
|
};
|