dsh-memu-memory 1.1.3 → 1.2.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/AGENTS.md +1 -1
- package/README.md +39 -6
- package/README.zh.md +39 -7
- package/docs/active-memory-ab.md +385 -0
- package/docs/binding-lifecycle.md +99 -0
- package/docs/contributing.md +3 -3
- package/docs/emotion-model-spec.md +281 -0
- package/docs/env-and-commands.md +8 -0
- package/docs/host-api.md +2 -2
- package/docs/memory-semantics.md +12 -10
- package/docs/recall-benchmark.md +238 -0
- package/docs/troubleshooting.md +396 -395
- package/lib/active-memory.js +165 -0
- package/lib/activity-log.js +23 -6
- package/lib/binding-diagnostics.js +8 -3
- package/lib/client.js +335 -35
- package/lib/consolidation-ledger.js +347 -0
- package/lib/consolidation-receipt.js +284 -0
- package/lib/consolidation-snapshot.js +274 -0
- package/lib/consolidation-worker.js +144 -0
- package/lib/curator.js +86 -31
- package/lib/cycle.js +40 -6
- package/lib/graph.js +139 -5
- package/lib/index.js +33 -1
- package/lib/inject.js +104 -76
- package/lib/local-embedding-manager.js +24 -6
- package/lib/memory.js +534 -88
- package/lib/mood.js +268 -39
- package/lib/preset-scope.js +1 -1
- package/lib/private.js +430 -43
- package/lib/prompts/private-consolidation.coding.md +1 -0
- package/lib/prompts/private-consolidation.md +1 -0
- package/lib/prompts/private-consolidation.mixed.md +1 -0
- package/lib/prompts/private-consolidation.rp.md +1 -0
- package/lib/prompts.js +4 -4
- package/lib/remote-summary.js +51 -25
- package/lib/rpc.js +54 -5
- package/lib/state.js +29 -12
- package/lib/text-similarity.js +106 -0
- package/lib/tools.js +113 -10
- package/lib/vector-migration.js +28 -3
- package/lib/vector-profile.js +51 -0
- package/package.json +4 -2
- package/scripts/acceptance-local.mjs +2 -1
- package/scripts/active-memory-ab-live.mjs +655 -0
- package/scripts/active-memory-ab.mjs +1302 -0
- package/scripts/chain-matrix.mjs +11 -1
- package/scripts/check-all.mjs +10 -1
- package/scripts/replay.mjs +573 -0
- package/tests/activation.test.mjs +89 -0
- package/tests/active-memory-ab.test.mjs +745 -0
- package/tests/active-memory.test.mjs +129 -0
- package/tests/activity-log.test.mjs +126 -0
- package/tests/activity-wiring.test.mjs +103 -0
- package/tests/appendix-b.test.mjs +216 -0
- package/tests/appendix-b2.test.mjs +166 -0
- package/tests/audit-regression.test.mjs +272 -0
- package/tests/audit-safety.test.mjs +227 -0
- package/tests/backend-compensation.test.mjs +272 -0
- package/tests/backup.test.mjs +793 -0
- package/tests/binding-diagnostics.test.mjs +720 -0
- package/tests/browser.test.mjs +366 -0
- package/tests/client-render.test.mjs +892 -0
- package/tests/coalesce.test.mjs +113 -0
- package/tests/config.test.mjs +159 -0
- package/tests/consolidation-credential.test.mjs +331 -0
- package/tests/consolidation-feed.test.mjs +96 -0
- package/tests/consolidation-fork.test.mjs +489 -0
- package/tests/consolidation-ledger.test.mjs +298 -0
- package/tests/consolidation-snapshot.test.mjs +360 -0
- package/tests/consolidation-value.test.mjs +89 -0
- package/tests/context-scope.test.mjs +40 -0
- package/tests/cue.test.mjs +44 -0
- package/tests/curator-commit.test.mjs +277 -0
- package/tests/curator-probe.test.mjs +433 -0
- package/tests/curator.test.mjs +150 -0
- package/tests/cycle.test.mjs +411 -0
- package/tests/disclosure.test.mjs +231 -0
- package/tests/doc-drift.test.mjs +222 -0
- package/tests/embedding-catalog.test.mjs +266 -0
- package/tests/embedding-download.test.mjs +391 -0
- package/tests/embedding-runtime.test.mjs +389 -0
- package/tests/emotion-chain.test.mjs +84 -0
- package/tests/emotion-contract.test.mjs +445 -0
- package/tests/engine.test.mjs +35 -0
- package/tests/fixtures/active-memory-ab/README.md +56 -0
- package/tests/fixtures/active-memory-ab/cases/01-clean-dense-turn.json +351 -0
- package/tests/fixtures/active-memory-ab/cases/02-clean-thin-turn.json +122 -0
- package/tests/fixtures/active-memory-ab/cases/03-clean-starved-small.json +125 -0
- package/tests/fixtures/active-memory-ab/cases/04-clean-large-thin.json +137 -0
- package/tests/fixtures/active-memory-ab/cases/05-clean-half-step.json +166 -0
- package/tests/fixtures/active-memory-ab/cases/06-clean-duplicate-reread.json +197 -0
- package/tests/fixtures/active-memory-ab/cases/07-clean-recent-recall.json +125 -0
- package/tests/fixtures/active-memory-ab/cases/08-global-relevant.json +111 -0
- package/tests/fixtures/active-memory-ab/cases/09-global-irrelevant-conflict.json +158 -0
- package/tests/fixtures/active-memory-ab/cases/10-no-human-turn.json +97 -0
- package/tests/fixtures/active-memory-ab/cases/11-clean-scale-100.json +137 -0
- package/tests/fixtures/active-memory-ab/cases/12-clean-scale-1000.json +137 -0
- package/tests/fixtures/active-memory-ab/cases/13-clean-scale-5000.json +137 -0
- package/tests/fixtures/active-memory-ab/cases/14-clean-scale-20000.json +137 -0
- package/tests/fixtures/active-memory-ab/lead-ai-reviewed-reference.json +191 -0
- package/tests/fixtures/active-memory-ab/manifest.json +199 -0
- package/tests/fixtures/recall-bench/01-paraphrase.json +184 -0
- package/tests/fixtures/recall-bench/02-exact.json +153 -0
- package/tests/fixtures/recall-bench/03-negation.json +173 -0
- package/tests/fixtures/recall-bench/04-no-answer.json +162 -0
- package/tests/fixtures/recall-bench/05-conflict.json +194 -0
- package/tests/fixtures/recall-bench/06-boundary.json +273 -0
- package/tests/fixtures/recall-bench/07-secret.json +191 -0
- package/tests/fixtures/recall-bench/08-coding.json +164 -0
- package/tests/fixtures/recall-bench-gold-candidates/01-paraphrase.json +193 -0
- package/tests/fixtures/recall-bench-gold-candidates/02-exact.json +197 -0
- package/tests/fixtures/recall-bench-gold-candidates/03-negation.json +244 -0
- package/tests/fixtures/recall-bench-gold-candidates/04-no-answer.json +189 -0
- package/tests/fixtures/recall-bench-gold-candidates/05-conflict.json +245 -0
- package/tests/fixtures/recall-bench-gold-candidates/06-boundary.json +279 -0
- package/tests/fixtures/recall-bench-gold-candidates/07-secret.json +224 -0
- package/tests/fixtures/recall-bench-gold-candidates/08-coding.json +218 -0
- package/tests/fixtures/repetition-eval/01-zh-rp.json +93 -0
- package/tests/fixtures/repetition-eval/02-en.json +97 -0
- package/tests/fixtures/repetition-eval/03-code.json +91 -0
- package/tests/fixtures/repetition-eval/04-names.json +116 -0
- package/tests/fixtures/repetition-eval/05-user-quote.json +96 -0
- package/tests/fixtures/repetition-eval/06-agreement.json +86 -0
- package/tests/graph-cascade.test.mjs +190 -0
- package/tests/graph-view.test.mjs +187 -0
- package/tests/graph.test.mjs +94 -0
- package/tests/hook-order.test.mjs +72 -0
- package/tests/humanlike.test.mjs +232 -0
- package/tests/hybrid-recall.test.mjs +122 -0
- package/tests/inbound.test.mjs +393 -0
- package/tests/inject-guide.test.mjs +364 -0
- package/tests/inject.test.mjs +222 -0
- package/tests/injection-budget-settings.test.mjs +35 -0
- package/tests/injection-budget.test.mjs +64 -0
- package/tests/injection-chain.test.mjs +191 -0
- package/tests/loader-register.mjs +3 -0
- package/tests/loader.mjs +12 -0
- package/tests/local-embedding-entry.test.mjs +95 -0
- package/tests/local-embedding-manager.test.mjs +476 -0
- package/tests/local-embedding-protocol.test.mjs +95 -0
- package/tests/local-embedding-worker.test.mjs +346 -0
- package/tests/managed-memory.test.mjs +591 -0
- package/tests/managed-runtime.test.mjs +40 -0
- package/tests/media.test.mjs +132 -0
- package/tests/migration-payload.test.mjs +42 -0
- package/tests/mood.test.mjs +109 -0
- package/tests/multiprocess.test.mjs +29 -0
- package/tests/new-session-gate.test.mjs +361 -0
- package/tests/package-shipping.test.mjs +84 -0
- package/tests/preset-scope.test.mjs +212 -0
- package/tests/prompts.test.mjs +250 -0
- package/tests/react-snapshot.test.mjs +23 -0
- package/tests/recall-bench.test.mjs +306 -0
- package/tests/recall-dedup.test.mjs +417 -0
- package/tests/remote.test.mjs +140 -0
- package/tests/rename.test.mjs +30 -0
- package/tests/repetition-detector-snapshot.test.mjs +231 -0
- package/tests/repetition-eval-live.test.mjs +177 -0
- package/tests/repetition-eval.test.mjs +373 -0
- package/tests/salient-date.test.mjs +142 -0
- package/tests/settings-beat.test.mjs +221 -0
- package/tests/short-term.test.mjs +166 -0
- package/tests/sidebar-beat.test.mjs +212 -0
- package/tests/sidebar-refresh.test.mjs +123 -0
- package/tests/sidebar.test.mjs +70 -0
- package/tests/snippet.test.mjs +47 -0
- package/tests/source-kind.test.mjs +52 -0
- package/tests/state.test.mjs +150 -0
- package/tests/stats.test.mjs +112 -0
- package/tests/storage.test.mjs +125 -0
- package/tests/stubs/dsh-llm.js +5 -0
- package/tests/stubs/dsh-session.js +7 -0
- package/tests/stubs/dsh-tools.js +101 -0
- package/tests/tool-schema.test.mjs +73 -0
- package/tests/tools.test.mjs +184 -0
- package/tests/ui-defaults.test.mjs +57 -0
- package/tests/update-fields.test.mjs +138 -0
- package/tests/vector-migration.test.mjs +790 -0
- package/tests/vector-profile.test.mjs +425 -0
- package/tests/visibility-migration.test.mjs +152 -0
- package/tests/webui-auth.test.mjs +454 -0
- package/tests/webui.test.mjs +165 -0
- package/tests/windows-env.test.mjs +89 -0
package/lib/tools.js
CHANGED
|
@@ -80,6 +80,28 @@ function salientTag(entry) {
|
|
|
80
80
|
return isDatedSalientDate(entry.salientDate) ? `date=${entry.salientDate}` : `anniversary=${entry.salientDate}`
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
// 四轴名在展示层就地映射:这是给人读的一行摘要,不是本地化词条。
|
|
84
|
+
const EMOTION_LABELS = { joy: '喜', anger: '怒', sorrow: '哀', fear: '惧' }
|
|
85
|
+
const EMOTION_AXES_IN_ORDER = ['joy', 'anger', 'sorrow', 'fear']
|
|
86
|
+
|
|
87
|
+
// 只留够显眼的份额,最多两项:四项全列会把一行标签读成噪声。
|
|
88
|
+
function emotionBrief(entry) {
|
|
89
|
+
const emotion = entry.emotion
|
|
90
|
+
if (!emotion || typeof emotion !== 'object') return ''
|
|
91
|
+
const shares = EMOTION_AXES_IN_ORDER
|
|
92
|
+
.map((axis) => ({ axis, share: Number(emotion[axis]) || 0 }))
|
|
93
|
+
.filter((item) => item.share >= 0.05)
|
|
94
|
+
.sort((a, b) => b.share - a.share)
|
|
95
|
+
.slice(0, 2)
|
|
96
|
+
if (!shares.length) return ''
|
|
97
|
+
const parts = shares.map((item) => `${EMOTION_LABELS[item.axis]}${item.share.toFixed(2)}`)
|
|
98
|
+
const dominance = Number(entry.dominance)
|
|
99
|
+
if (Number.isFinite(dominance)) parts.push(`dom${dominance >= 0 ? '+' : ''}${dominance.toFixed(2)}`)
|
|
100
|
+
const arousal = Number(entry.arousal)
|
|
101
|
+
if (Number.isFinite(arousal)) parts.push(`a${arousal.toFixed(2)}`)
|
|
102
|
+
return parts.join(' · ')
|
|
103
|
+
}
|
|
104
|
+
|
|
83
105
|
function formatPrivateItem(item, index, options = {}) {
|
|
84
106
|
const entry = item.entry || {}
|
|
85
107
|
const lines = [
|
|
@@ -107,7 +129,10 @@ function formatPrivateItem(item, index, options = {}) {
|
|
|
107
129
|
// way made "the test is tomorrow" read like a recurring commemorative date.
|
|
108
130
|
const tag = salientTag(entry)
|
|
109
131
|
if (tag) lines.push(` ${tag}`)
|
|
110
|
-
|
|
132
|
+
// 有构成就讲构成:同一个 valence 可能来自怒,也可能来自哀,一个数字分不出来。
|
|
133
|
+
const brief = emotionBrief(entry)
|
|
134
|
+
if (brief) lines.push(` emo=${brief}`)
|
|
135
|
+
else if (typeof entry.moodValence === 'number' && entry.moodValence !== 0) lines.push(` mood=${entry.moodValence.toFixed(2)}`)
|
|
111
136
|
if (item.content) {
|
|
112
137
|
const body = options.query
|
|
113
138
|
? centeredSnippet(item.content, options.query)
|
|
@@ -137,9 +162,13 @@ function formatGlobalItem(item, index, options = {}) {
|
|
|
137
162
|
return lines.join('\n')
|
|
138
163
|
}
|
|
139
164
|
|
|
140
|
-
export function registerTools(ctx) {
|
|
165
|
+
export function registerTools(ctx, { only = null } = {}) {
|
|
141
166
|
const tools = []
|
|
142
|
-
|
|
167
|
+
// 全量注册(宿主装载)才更新全局 registrationReport:整理 worker 的按需挂载
|
|
168
|
+
// (private.js attachWorkerTools,only=读写四件)不能把 memu_status 的注册清单
|
|
169
|
+
// 覆盖成 4/4,否则其它会话看到的是 worker 的子集,可观测性就断了。
|
|
170
|
+
const reportSaved = only ? registrationReport : null
|
|
171
|
+
if (!only) registrationReport = { attempted: 0, registered: 0, failures: [] }
|
|
143
172
|
tools.push(toolBase(
|
|
144
173
|
'memu_private_link',
|
|
145
174
|
'Connect two existing memories or a memory and an existing entity in the bound library. kind is relation, commitment, event, person or conflict. Requires an evidence note of 8-500 characters; confidence defaults to 0.7. Cannot delete links or cross conversation visibility boundaries.',
|
|
@@ -187,9 +216,18 @@ export function registerTools(ctx) {
|
|
|
187
216
|
description: 'Where this came from: inferred (default — you concluded it), user_said (only when the words are actually theirs and quotable), imagined, or told.',
|
|
188
217
|
},
|
|
189
218
|
confidence: { type: 'number', description: '0-1 confidence in this memory. Below 0.45 it is injected as a hedge, so use it honestly.' },
|
|
190
|
-
mood_valence: { type: 'number', description: '
|
|
219
|
+
mood_valence: { type: 'number', description: '派生值:给了 emotion 时由四项算出,-1(坏)到 1(好)。省略则沿用当前会话读数。' },
|
|
191
220
|
arousal: { type: 'number', description: 'How strongly it hit, 0-1.' },
|
|
221
|
+
emotion: {
|
|
222
|
+
type: 'object',
|
|
223
|
+
properties: { joy: { type: 'number' }, anger: { type: 'number' }, sorrow: { type: 'number' }, fear: { type: 'number' } },
|
|
224
|
+
// 四个轴是封闭集合:多写的键只会被误当成第五种情绪。
|
|
225
|
+
additionalProperties: false,
|
|
226
|
+
description: '情绪构成:喜悦/愤怒/哀伤/恐惧,四项自动归一化到和为 1。填记忆所述那件事的情绪,不是此刻整理时的心情。省略=继承当前会话读数;显式全零=明确中性;工程事实请用中性,不要因为 bug 难修或结论重要就填负面情绪。',
|
|
227
|
+
},
|
|
228
|
+
dominance: { type: 'number', description: '谁在掌控:-1 完全失控 … +1 完全掌控。省略=继承当前会话。' },
|
|
192
229
|
peak: { type: 'boolean', description: 'A high point or low point of the arc — peaks and endings stay easier to recall.' },
|
|
230
|
+
allow_duplicate: { type: 'boolean', description: 'Only for two genuinely independent observations of the same fact (e.g. the same event recorded twice in one day). The default false refuses a near-identical rewrite and returns the existing memory id; passing true states you looked at that existing memory and still want a separate entry.' },
|
|
193
231
|
salient_date: { type: 'string', description: 'MM-DD for something that comes back every year (a birthday, an anniversary of an event); it resurfaces on that day each year. YYYY-MM-DD for a one-off dated commitment or appointment: it is raised the day before and on the day itself, and never again after it has passed. Use the annual form only when the day really does recur. A relative day (今天/明天/后天/today/tomorrow) must be resolved to the absolute date it points at: sending a bare MM-DD turns one appointment into a yearly reminder. If the content itself names a date, pass it here too: when this field is empty the plugin only falls back to a single unambiguous YYYY-MM-DD found in the text.' },
|
|
194
232
|
track: { type: 'string', description: 'memory (default) or habit for an aggregated expression/sticker habit; do not write one habit per occurrence.' },
|
|
195
233
|
attachments: { type: 'array', items: { type: 'object', properties: { sha256: { type: 'string', required: true } }, additionalProperties: true }, description: 'At most 8 sha256 references received in this conversation. Never supply paths or bytes. Ordinary image memories default to short_term if layer is omitted; choose long_term explicitly only when important.' },
|
|
@@ -219,9 +257,23 @@ export function registerTools(ctx) {
|
|
|
219
257
|
confidence: args.confidence,
|
|
220
258
|
moodValence: args.mood_valence ?? null,
|
|
221
259
|
arousal: args.arousal ?? null,
|
|
260
|
+
// `undefined` means "the model said nothing" and is allowed to inherit the
|
|
261
|
+
// turn's mood; `null` means "explicitly neutral" and must survive as such. Using
|
|
262
|
+
// `?? null` here would erase that difference and re-inherit on a neutral call.
|
|
263
|
+
emotion: args.emotion,
|
|
264
|
+
dominance: args.dominance ?? null,
|
|
222
265
|
peak: args.peak === true,
|
|
223
266
|
salientDate: args.salient_date ?? null,
|
|
267
|
+
allowDuplicate: args.allow_duplicate === true,
|
|
224
268
|
})
|
|
269
|
+
// 事实级去重:几乎相同的重写被拒,返回的是
|
|
270
|
+
// { deduped, existingId, hint },没有 entry——必须先接住,否则 result.entry.layer 抛错。
|
|
271
|
+
if (result.deduped === true) {
|
|
272
|
+
return [
|
|
273
|
+
`未写入:库内已有几乎相同的记忆 [${result.existingId}](相似度 ${(Number(result.similarity) || 0).toFixed(2)})。`,
|
|
274
|
+
`要补充内容请改用 memu_private_update 传该 id 合并;若确属两次独立观测,带 allow_duplicate=true 重写。`,
|
|
275
|
+
].join('\n')
|
|
276
|
+
}
|
|
225
277
|
const entry = result.entry
|
|
226
278
|
if (result.conflict) {
|
|
227
279
|
// The entry is held (pending_review): it is stored, but excluded from
|
|
@@ -309,6 +361,13 @@ export function registerTools(ctx) {
|
|
|
309
361
|
peak: { type: 'boolean', description: 'Flashbulb flag: the moment was charged. Changing it recomputes accuracy.' },
|
|
310
362
|
mood_valence: { type: 'number', description: 'How the moment felt, -1..1. Used as a retrieval lens; omitted keeps the stored value.' },
|
|
311
363
|
arousal: { type: 'number', description: 'How charged the moment was, 0..1. Used as a retrieval lens; omitted keeps the stored value.' },
|
|
364
|
+
emotion: {
|
|
365
|
+
type: 'object',
|
|
366
|
+
properties: { joy: { type: 'number' }, anger: { type: 'number' }, sorrow: { type: 'number' }, fear: { type: 'number' } },
|
|
367
|
+
additionalProperties: false,
|
|
368
|
+
description: '情绪构成,三项语义不同:省略=保留原值;显式 null=清除 emotion,来源变 unknown;显式对象=校验并归一化后整份替换。改的是那条记忆所述事件的感受,不是此刻的心情。',
|
|
369
|
+
},
|
|
370
|
+
dominance: { type: 'number', description: '谁在掌控:-1 完全失控 … +1 完全掌控。省略=保留原值。' },
|
|
312
371
|
salient_date: { type: 'string', description: 'Re-date the memory: MM-DD for an annual anniversary, YYYY-MM-DD for a one-off dated commitment, or an empty string to remove the date. Use this to repair a date that was stored without its year.' },
|
|
313
372
|
},
|
|
314
373
|
async (args, exec) => {
|
|
@@ -318,6 +377,8 @@ export function registerTools(ctx) {
|
|
|
318
377
|
layer: args.layer, owner: args.owner, visibility: args.visibility,
|
|
319
378
|
source: args.source, track: args.track, peak: args.peak,
|
|
320
379
|
moodValence: args.mood_valence, arousal: args.arousal,
|
|
380
|
+
// undefined 就是"省略",必须原样传下去;换成 null 会把保留变成清除。
|
|
381
|
+
emotion: args.emotion, dominance: args.dominance,
|
|
321
382
|
// Sentinel: absent means "leave the date as it is", '' means "clear it".
|
|
322
383
|
salientDate: args.salient_date,
|
|
323
384
|
})
|
|
@@ -454,17 +515,59 @@ export function registerTools(ctx) {
|
|
|
454
515
|
{ isConcurrencySafe: () => true },
|
|
455
516
|
))
|
|
456
517
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
518
|
+
// 整理 worker 的挂载面(private.js 的 fork 兼容):只挂读写四件,不把
|
|
519
|
+
// archive/delete/unpin 这些破坏性工具暴露给一个后台子会话。
|
|
520
|
+
const wanted = Array.isArray(only) && only.length > 0 ? only : null
|
|
521
|
+
const usable = tools.filter((tool) => tool && (!wanted || wanted.includes(tool.name)))
|
|
522
|
+
if (only) {
|
|
523
|
+
registrationReport = reportSaved
|
|
524
|
+
if (usable.length !== wanted.length) {
|
|
525
|
+
try {
|
|
526
|
+
ctx?.logger?.warn?.(`dsh-memu-memory: worker tool mount compiled ${usable.length}/${wanted.length}: ${wanted.join(', ')}`)
|
|
527
|
+
} catch {}
|
|
528
|
+
}
|
|
529
|
+
} else {
|
|
530
|
+
registrationReport.registered = usable.length
|
|
531
|
+
if (registrationReport.failures.length > 0) {
|
|
532
|
+
try {
|
|
533
|
+
ctx?.logger?.warn?.(`dsh-memu-memory: host schema DSL refused ${registrationReport.failures.length} of ${registrationReport.attempted} tools: ${registrationReport.failures.map((failure) => `${failure.name} (${failure.message})`).join('; ')}`)
|
|
534
|
+
} catch {}
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
// register 这一步也逐件 fail-open(与 toolBase 的编译 fail-open 同一哲学):宿主
|
|
538
|
+
// 注册表拒一件(重名、scope 锁定)不能掀翻整组挂载——原来的 `.map()` 一件抛错就连
|
|
539
|
+
// 前面已注册成功的 disposer 一起丢掉,在 worker scope 上留下卸不掉的常驻注册。
|
|
540
|
+
const disposers = []
|
|
541
|
+
const attached = []
|
|
542
|
+
const refused = []
|
|
543
|
+
for (const tool of usable) {
|
|
544
|
+
try {
|
|
545
|
+
const dispose = ctx.tools.register(tool)
|
|
546
|
+
disposers.push(typeof dispose === 'function' ? dispose : null)
|
|
547
|
+
attached.push(tool.name)
|
|
548
|
+
} catch (error) {
|
|
549
|
+
refused.push(`${tool.name} (${String(error?.message || error)})`)
|
|
550
|
+
// 全量装载(宿主)才进 registrationReport:memu_status 的 refused 清单要如实反映
|
|
551
|
+
// 编译 + register 两道关都被拒了谁;worker 的按需挂载照旧不碰全局报告。
|
|
552
|
+
if (!only) registrationReport.failures.push({ name: tool.name, message: String(error?.message || error) })
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
if (refused.length > 0) {
|
|
556
|
+
if (!only) registrationReport.registered = attached.length
|
|
460
557
|
try {
|
|
461
|
-
ctx?.logger?.warn?.(`dsh-memu-memory:
|
|
558
|
+
ctx?.logger?.warn?.(`dsh-memu-memory: tool mount refused ${refused.length}/${usable.length} at register: ${refused.join('; ')}`)
|
|
462
559
|
} catch {}
|
|
463
560
|
}
|
|
464
|
-
|
|
465
|
-
|
|
561
|
+
// 返回值仍是可直接调用的 disposer(private.js attachWorkerTools 与宿主装载都这么用),
|
|
562
|
+
// 但附挂结构化名单:attached = 编译 + register 都真正成功的工具名;requested = 请求
|
|
563
|
+
// 名单(宿主全量装载没有 only,为 null)。部分编译/注册失败时调用方据此写活动日志,
|
|
564
|
+
// 不再把 wanted 全量误报成已挂载。
|
|
565
|
+
const disposeAll = () => {
|
|
466
566
|
for (const dispose of disposers) {
|
|
467
567
|
try { dispose?.() } catch {}
|
|
468
568
|
}
|
|
469
569
|
}
|
|
570
|
+
disposeAll.attached = attached
|
|
571
|
+
disposeAll.requested = wanted
|
|
572
|
+
return disposeAll
|
|
470
573
|
}
|
package/lib/vector-migration.js
CHANGED
|
@@ -849,8 +849,13 @@ export function createVectorMigrator(options = {}) {
|
|
|
849
849
|
ledger: readLedger,
|
|
850
850
|
/**
|
|
851
851
|
* 当前生效代际里"哪些 id 已经写进向量库"(按 scope key:libraryId 或 'global')。
|
|
852
|
-
*
|
|
853
|
-
*
|
|
852
|
+
* 两条腿合成,缺一都会数错:
|
|
853
|
+
* * 账本腿:迁移写库时刻意不碰索引,所以迁移写入的条目没有 entry.vector 戳,
|
|
854
|
+
* 只能查账本。但永久删除(forget)只删引擎行和索引行、不动账本,所以账本里的
|
|
855
|
+
* id 还要扣掉"索引里已经没有的"(删掉的条目必然已从索引移除)。
|
|
856
|
+
* * 戳腿:迁移之后的写入(remember/import/review/update/curator)在索引条目上带
|
|
857
|
+
* entry.vector 戳。只认当前代际的戳 —— 旧代际的戳说明它属于旧空间,是否还在
|
|
858
|
+
* 当前空间由账本腿回答(切代际会把当时可准入的条目全部重写一遍并记账)。
|
|
854
859
|
*/
|
|
855
860
|
membership: () => {
|
|
856
861
|
const state = vectorState()
|
|
@@ -858,10 +863,30 @@ export function createVectorMigrator(options = {}) {
|
|
|
858
863
|
if (!active) return null
|
|
859
864
|
const ledger = readLedger({ fingerprint: active.fingerprint, generation: active.generation })
|
|
860
865
|
if (!ledger) return null
|
|
866
|
+
const generation = Number(active.generation)
|
|
861
867
|
const out = {}
|
|
862
868
|
for (const scope of scopesFor(active.databases)) {
|
|
863
869
|
const bucket = ledger.libraries?.[scope.key]
|
|
864
|
-
|
|
870
|
+
const migrated = bucket?.items && typeof bucket.items === 'object' ? Object.keys(bucket.items) : []
|
|
871
|
+
let entries = null
|
|
872
|
+
try {
|
|
873
|
+
const loaded = impl.loadIndex(scope.libraryId)
|
|
874
|
+
// loadIndex 的约定是同步读盘;拿到 Promise(测试替身)时按读不到处理,不猜。
|
|
875
|
+
entries = loaded && typeof loaded.then === 'function' ? null : (loaded?.entries || {})
|
|
876
|
+
} catch { entries = null }
|
|
877
|
+
const ids = new Set()
|
|
878
|
+
for (const id of migrated) {
|
|
879
|
+
// 索引读不出来时宁可贵一点按"还在"算:损坏是暂时的,误扣会让用户以为记忆丢了。
|
|
880
|
+
if (!entries || Object.prototype.hasOwnProperty.call(entries, id)) ids.add(id)
|
|
881
|
+
}
|
|
882
|
+
if (entries) {
|
|
883
|
+
for (const [id, entry] of Object.entries(entries)) {
|
|
884
|
+
// remote-api 的戳描述的是远端空间,不属于本地代际;词法路径没有戳。
|
|
885
|
+
if (!entry || typeof entry !== 'object' || entry.vector?.route !== 'managed-local') continue
|
|
886
|
+
if (Number(entry.vector.generation) === generation) ids.add(id)
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
out[scope.key] = [...ids]
|
|
865
890
|
}
|
|
866
891
|
return out
|
|
867
892
|
},
|
package/lib/vector-profile.js
CHANGED
|
@@ -522,6 +522,30 @@ export function activeGeneration() {
|
|
|
522
522
|
return state?.active?.generation ?? null
|
|
523
523
|
}
|
|
524
524
|
|
|
525
|
+
/**
|
|
526
|
+
* 条目级"已入向量库"戳(entry.vector)。迁移写库故意不碰索引,所以迁移写入的条目没有
|
|
527
|
+
* 戳、只有账本知道;而迁移之后的每一次向量写入(remember/import/review/update/curator)
|
|
528
|
+
* 都必须在提交成功后把这条戳落进索引。词法路径没写库,不戳;remote-api 沿用历史口径
|
|
529
|
+
* (本地还留着 active 代际时照样记,界面只拿它回答"进没进向量后端")。空间归属由
|
|
530
|
+
* membership() 的戳腿自行过滤:只认 route === 'managed-local' 且代际匹配的戳。只读不抛错。
|
|
531
|
+
*/
|
|
532
|
+
export function activeEntryStamp(route) {
|
|
533
|
+
if (route === 'lexical') return null
|
|
534
|
+
try {
|
|
535
|
+
const active = readVectorState({ tolerateMissing: true }).state?.active
|
|
536
|
+
if (!active) return null
|
|
537
|
+
return {
|
|
538
|
+
route,
|
|
539
|
+
model: active.profile?.catalogId || null,
|
|
540
|
+
generation: active.generation,
|
|
541
|
+
fingerprint: String(active.fingerprint || '').slice(0, 8),
|
|
542
|
+
at: stamp(),
|
|
543
|
+
}
|
|
544
|
+
} catch {
|
|
545
|
+
return null
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
525
549
|
/** active manifest 里的 DB 集合,一次性取得(避免多库逐个换混空间)。 */
|
|
526
550
|
export function activeDatabases() {
|
|
527
551
|
return databaseMapFor(vectorState()?.active)
|
|
@@ -565,6 +589,33 @@ export function dbPathFor(libraryId = null) {
|
|
|
565
589
|
* loadIndex must be synchronous; by default the authoritative raw index is read.
|
|
566
590
|
* A nonempty, corrupt, quarantined or unknown-shaped index requires rebuilding.
|
|
567
591
|
*/
|
|
592
|
+
/**
|
|
593
|
+
* Remove one library from the active database map under the manifest lock.
|
|
594
|
+
* The caller owns filesystem cleanup; returning the captured absolute path keeps
|
|
595
|
+
* the manifest mutation atomic without deleting a possibly newer generation.
|
|
596
|
+
*/
|
|
597
|
+
export function removeActiveLibrary(libraryId) {
|
|
598
|
+
if (!isSafeId(libraryId) || libraryId === 'global') throw vectorError(VECTOR_ERROR_CODES.PATH_ESCAPE, 'unsafe private library id: ' + JSON.stringify(libraryId))
|
|
599
|
+
let removed = null
|
|
600
|
+
updateVectorStateSync(state => {
|
|
601
|
+
const guard = liveGuard(state.writeGuard)
|
|
602
|
+
if (guard.frozen || Object.keys(guard.leases).length) {
|
|
603
|
+
throw vectorError('VECTOR_WRITE_BUSY', 'an in-flight vector write prevents deleting a library; retry shortly', { libraryId })
|
|
604
|
+
}
|
|
605
|
+
if (state.pending) {
|
|
606
|
+
throw vectorError('VECTOR_WRITE_BUSY', 'a vector generation switch prevents deleting a library; retry shortly', { libraryId })
|
|
607
|
+
}
|
|
608
|
+
const active = state.active
|
|
609
|
+
const entry = active?.databases?.libraries?.[libraryId]
|
|
610
|
+
if (!active || !entry) return null
|
|
611
|
+
removed = resolveVectorPath(entry.path)
|
|
612
|
+
const libraries = { ...active.databases.libraries }
|
|
613
|
+
delete libraries[libraryId]
|
|
614
|
+
return { ...state, active: { ...active, databases: { ...active.databases, libraries } } }
|
|
615
|
+
})
|
|
616
|
+
return removed
|
|
617
|
+
}
|
|
618
|
+
|
|
568
619
|
export function enrollActiveLibrary(libraryId, { loadIndex } = {}) {
|
|
569
620
|
if (!isSafeId(libraryId) || libraryId === 'global') throw vectorError(VECTOR_ERROR_CODES.PATH_ESCAPE, 'unsafe private library id: ' + JSON.stringify(libraryId))
|
|
570
621
|
let db = null
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-memu-memory",
|
|
3
3
|
"icon": "./assets/icon.png",
|
|
4
|
-
"version": "1.1
|
|
4
|
+
"version": "1.2.1",
|
|
5
5
|
"description": "面向长篇 RP 与陪伴对话的 DeepSeek Harness 记忆插件:三层记忆、逐轮召回、来源标注、关系图、图片附件与独立 WebUI。",
|
|
6
6
|
"keywords": [
|
|
7
7
|
"deepseek-harness",
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
"files": [
|
|
46
46
|
"lib",
|
|
47
47
|
"scripts",
|
|
48
|
+
"tests",
|
|
48
49
|
"cordis.patch.yml",
|
|
49
50
|
"requirements.lock",
|
|
50
51
|
"README.md",
|
|
@@ -60,7 +61,8 @@
|
|
|
60
61
|
"check": "node scripts/check-all.mjs",
|
|
61
62
|
"setup": "node scripts/setup.mjs",
|
|
62
63
|
"test": "node --import ./tests/loader-register.mjs --test tests/*.test.mjs",
|
|
63
|
-
"test:chain": "node scripts/chain-matrix.mjs"
|
|
64
|
+
"test:chain": "node scripts/chain-matrix.mjs",
|
|
65
|
+
"replay": "node --import ./tests/loader-register.mjs scripts/replay.mjs"
|
|
64
66
|
},
|
|
65
67
|
"engines": {
|
|
66
68
|
"node": ">=20.9.0",
|
|
@@ -699,7 +699,8 @@ await auto('G-1', 'the kept docs exist and nothing points at a retired one', asy
|
|
|
699
699
|
for (const rel of ['assets/icon.png', 'assets/icon-256.png', 'assets/icon.svg']) {
|
|
700
700
|
assert.ok(fs.existsSync(path.join(ROOT, rel)), rel + ' exists')
|
|
701
701
|
}
|
|
702
|
-
|
|
702
|
+
// Since 1.1.3 the changelog is the source for release notes (release.yml), so it is kept.
|
|
703
|
+
assert.ok(fs.existsSync(path.join(ROOT, 'CHANGELOG' + '.md')), 'CHANGELOG.md carries release notes')
|
|
703
704
|
// 细节从 AGENTS.md 搬进了 docs/:索引必须短,指针必须都能落地
|
|
704
705
|
const agents = fs.readFileSync(path.join(ROOT, 'AGENTS.md'), 'utf8')
|
|
705
706
|
assert.ok(agents.includes('plugin:dsh-memu-memory'), 'the notes carry the real source kind, not the old one')
|