dsh-memu-memory 1.1.3 → 1.2.0
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 +31 -2
- package/README.zh.md +30 -2
- package/docs/active-memory-ab.md +385 -0
- package/docs/binding-lifecycle.md +99 -0
- package/docs/contributing.md +1 -1
- package/docs/emotion-model-spec.md +281 -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 +395 -395
- package/lib/active-memory.js +165 -0
- package/lib/activity-log.js +1 -1
- package/lib/binding-diagnostics.js +8 -3
- package/lib/client.js +33 -11
- 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 +115 -0
- package/lib/cycle.js +40 -6
- package/lib/inject.js +104 -76
- package/lib/local-embedding-manager.js +24 -6
- package/lib/memory.js +221 -19
- package/lib/mood.js +268 -39
- package/lib/preset-scope.js +1 -1
- package/lib/private.js +335 -40
- 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 +9 -1
- package/lib/state.js +29 -12
- package/lib/tools.js +49 -2
- package/lib/vector-profile.js +27 -0
- package/package.json +2 -1
- 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/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 +212 -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 +320 -0
- package/tests/consolidation-fork.test.mjs +357 -0
- package/tests/consolidation-ledger.test.mjs +298 -0
- package/tests/consolidation-snapshot.test.mjs +321 -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-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-view.test.mjs +187 -0
- package/tests/graph.test.mjs +77 -0
- package/tests/hook-order.test.mjs +72 -0
- package/tests/humanlike.test.mjs +232 -0
- package/tests/hybrid-recall.test.mjs +92 -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 +536 -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/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 +208 -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 +733 -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 +164 -0
- package/tests/windows-env.test.mjs +89 -0
package/lib/remote-summary.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { getSummaryConfig } from './memu-config.js'
|
|
2
2
|
import { loadPromptForDomain, CONSOLIDATION_CONTRACTS } from './prompts.js'
|
|
3
3
|
import { getSettings } from './state.js'
|
|
4
|
+
import { normalizeEmotion } from './mood.js'
|
|
5
|
+
import { record } from './activity-log.js'
|
|
4
6
|
|
|
5
7
|
function parseJsonObject(text) {
|
|
6
8
|
const raw = String(text || '').trim()
|
|
@@ -12,31 +14,42 @@ function parseJsonObject(text) {
|
|
|
12
14
|
try { return JSON.parse(raw.slice(start, end + 1)) } catch { return null }
|
|
13
15
|
}
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
// 逐条清洗。非法字段不修,只把这一条连同原因丢进 problems:一条坏数据不该让
|
|
18
|
+
// 整批整理白跑,也不该被悄悄改成"看起来合法"的情绪留在库里。
|
|
19
|
+
function cleanMemories(value, problems = []) {
|
|
16
20
|
const list = Array.isArray(value) ? value : []
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
21
|
+
const out = []
|
|
22
|
+
list.forEach((m, index) => {
|
|
23
|
+
try {
|
|
24
|
+
out.push({
|
|
25
|
+
title: String(m?.title || '').trim().slice(0, 120),
|
|
26
|
+
content: String(m?.content || '').trim(),
|
|
27
|
+
tags: Array.isArray(m?.tags) ? m.tags.slice(0, 20).map((t) => String(t)) : [],
|
|
28
|
+
openLoop: m?.open_loop === true || m?.openLoop === true,
|
|
29
|
+
confidence: Number.isFinite(Number(m?.confidence)) ? Number(m.confidence) : 0.8,
|
|
30
|
+
layer: ['long_term', 'short_term'].includes(m?.layer) ? m.layer : 'long_term',
|
|
31
|
+
disclosure: ['can_tell', 'only_if_asked', 'never_say'].includes(m?.disclosure) ? m.disclosure : 'can_tell',
|
|
32
|
+
// A consolidation pass that forgets the field must not invent a scope: the default
|
|
33
|
+
// here has to match the tool path (`privateRemember`, which defaults to 'shared').
|
|
34
|
+
// 'session' used to be the fallback, which silently turned every sloppy summary into
|
|
35
|
+
// a memory that died with its conversation (and the level itself is gone now).
|
|
36
|
+
visibility: ['shared', 'secret'].includes(m?.visibility) ? m.visibility : 'shared',
|
|
37
|
+
owner: ['me', 'us', 'about_user', 'about_project'].includes(m?.owner) ? m.owner : 'me',
|
|
38
|
+
source: ['user_said', 'inferred', 'imagined', 'told'].includes(m?.source) ? m.source : 'inferred',
|
|
39
|
+
// 缺字段是"没读到情绪",null 才是;这里不能拿 valence 反推四轴。
|
|
40
|
+
emotion: normalizeEmotion(m?.emotion ?? null),
|
|
41
|
+
// 缺失或非法给 null 而不是 0:0 表示"明确中性",不是"不知道"。
|
|
42
|
+
dominance: Number.isFinite(m?.dominance) ? Math.max(-1, Math.min(1, m.dominance)) : null,
|
|
43
|
+
moodValence: Number.isFinite(m?.mood_valence) ? Math.max(-1, Math.min(1, m.mood_valence)) : null,
|
|
44
|
+
arousal: Number.isFinite(m?.arousal) ? Math.max(0, Math.min(1, m.arousal)) : null,
|
|
45
|
+
peak: m?.peak === true,
|
|
46
|
+
salientDate: typeof m?.salient_date === 'string' ? m.salient_date : null,
|
|
47
|
+
})
|
|
48
|
+
} catch (error) {
|
|
49
|
+
problems.push({ index, title: String(m?.title || '').slice(0, 120), error: String(error?.message || error) })
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
return out.filter((m) => m.content).slice(0, 20)
|
|
40
53
|
}
|
|
41
54
|
|
|
42
55
|
// Remote mode: the configured chat endpoint does the summarizing. The session
|
|
@@ -87,7 +100,20 @@ export async function summarizeRemote({ transcript, libraryName, domain, signal,
|
|
|
87
100
|
const text = data?.choices?.[0]?.message?.content
|
|
88
101
|
const parsed = parseJsonObject(text)
|
|
89
102
|
if (!parsed) throw new Error(`remote summary returned non-JSON: ${String(text || body).slice(0, 600)}`)
|
|
90
|
-
|
|
103
|
+
const problems = []
|
|
104
|
+
const memories = cleanMemories(parsed.memories, problems)
|
|
105
|
+
if (problems.length) {
|
|
106
|
+
// 远程模型的错误只能靠日志查,静默丢弃等于把问题留给下一个人复现。
|
|
107
|
+
record({
|
|
108
|
+
kind: 'consolidate',
|
|
109
|
+
scope: 'private',
|
|
110
|
+
ok: false,
|
|
111
|
+
level: 'warn',
|
|
112
|
+
message: `远程整理有 ${problems.length} 条被拒绝:${problems[0].error}`,
|
|
113
|
+
detail: { rejected: problems },
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
return memories
|
|
91
117
|
} finally {
|
|
92
118
|
clearTimeout(timer)
|
|
93
119
|
signal?.removeEventListener?.('abort', onAbort)
|
package/lib/rpc.js
CHANGED
|
@@ -86,6 +86,11 @@ async function privateListView(scope) {
|
|
|
86
86
|
decay: Math.min(1, Math.max(0, decayStrength(e) / 1.5)),
|
|
87
87
|
moodValence: e?.moodValence ?? null,
|
|
88
88
|
arousal: e?.arousal ?? null,
|
|
89
|
+
// 构成与掌控感分开投影:同一个 valence 说得出"多难过",说不出"是怒还是怕"。
|
|
90
|
+
emotion: e?.emotion ?? null,
|
|
91
|
+
dominance: e?.dominance ?? null,
|
|
92
|
+
// 来源是审计字段,只读:面板显示"这个情绪是谁给的",不给改。
|
|
93
|
+
emotionSource: e?.emotionSource ?? null,
|
|
89
94
|
peak: e?.peak === true,
|
|
90
95
|
salientDate: e?.salientDate || null,
|
|
91
96
|
attachments: attachmentPreview(libraryId, e || {}),
|
|
@@ -453,6 +458,7 @@ async function dispatch(ctx, endpoint, payload = {}) {
|
|
|
453
458
|
} catch {}
|
|
454
459
|
const rows = Object.entries(state.sessions || {})
|
|
455
460
|
.map(([sessionId, binding]) => {
|
|
461
|
+
if (binding.consolidationFork) return null
|
|
456
462
|
const inFolder = binding.scopeKey === scopeKey
|
|
457
463
|
|| (binding.libraryId && state.libraries?.[binding.libraryId]?.scopeKey === scopeKey)
|
|
458
464
|
if (!inFolder) return null
|
|
@@ -771,7 +777,9 @@ async function dispatch(ctx, endpoint, payload = {}) {
|
|
|
771
777
|
// `source` is writable: correcting provenance is the point of showing it.
|
|
772
778
|
// `salientDate` is writable for the same reason: an entry whose year was
|
|
773
779
|
// dropped has to be repairable from the browser, not only from JSON.
|
|
774
|
-
|
|
780
|
+
// `emotion`/`dominance` 可写:面板修的是那条记忆所述事件的感受。
|
|
781
|
+
// `emotionSource` 故意不在名单里:来源是审计结论,能改就等于能伪造。
|
|
782
|
+
const allowed = ['content', 'title', 'tags', 'status', 'openLoop', 'disclosure', 'confidence', 'owner', 'visibility', 'layer', 'source', 'salientDate', 'track', 'peak', 'moodValence', 'arousal', 'emotion', 'dominance', 'attachments', 'context']
|
|
775
783
|
// An unknown key used to be dropped in silence, so a typo here or in a script
|
|
776
784
|
// came back as a successful save that changed nothing. Refuse instead: the
|
|
777
785
|
// caller can only fix a field name it is told is wrong.
|
package/lib/state.js
CHANGED
|
@@ -10,7 +10,8 @@ import {
|
|
|
10
10
|
pruneBackups, restoreFromBackup, withBackupLock,
|
|
11
11
|
} from './backup.js'
|
|
12
12
|
import { migrateIndexVisibility } from './visibility.js'
|
|
13
|
-
import { memuDbFor, memuGlobalDbFor } from './vector-profile.js'
|
|
13
|
+
import { memuDbFor, memuGlobalDbFor, removeActiveLibrary } from './vector-profile.js'
|
|
14
|
+
import { normalizeEmotion, derivedValence } from './mood.js'
|
|
14
15
|
|
|
15
16
|
const STATE_FILE = () => path.join(dataDir(), 'state.json')
|
|
16
17
|
const GLOBAL_DB = () => path.join(dataDir(), 'global.sqlite3')
|
|
@@ -94,6 +95,12 @@ export const DEFAULT_SETTINGS = {
|
|
|
94
95
|
// "gone", and short enough that a conversation which ended gets tidied in the
|
|
95
96
|
// same sitting instead of waiting for a quiet hour that may never arrive.
|
|
96
97
|
sleepAfterIdleMinutes: 3,
|
|
98
|
+
// 主动记忆:分数门槛、饥饿阈值(空闲超过它无条件检查)、两种抑制冷却(分钟;整理冷却默认 0,
|
|
99
|
+
// 薄证据的重复 fork 由评分拦截,只有用户显式配置才启用硬冷却)。
|
|
100
|
+
activeMemoryMinScore: 0.8,
|
|
101
|
+
activeMemoryStarvationMinutes: 15,
|
|
102
|
+
activeMemoryCooldownMinutes: 0,
|
|
103
|
+
activeMemoryRecallCooldownMinutes: 15,
|
|
97
104
|
autoCurateEveryHours: 0,
|
|
98
105
|
autoCurateSessionLimit: 8,
|
|
99
106
|
// 'local' keeps every memory on this machine; 'cloud' hands MemU's own backend
|
|
@@ -343,6 +350,8 @@ export function updateSettings(patch) {
|
|
|
343
350
|
mustRememberCap: [1, 100], randomCueChance: [0, 1], shortTermTtlHours: [0, 8760],
|
|
344
351
|
unpinBudgetPerSession: [1, 20], unpinCooldownMinutes: [0, 1440], sleepAfterIdleMinutes: [1, 1440],
|
|
345
352
|
autoCurateEveryHours: [0, 8760], autoCurateSessionLimit: [1, 10], purgeArchivedAfterDays: [0, 36500],
|
|
353
|
+
activeMemoryMinScore: [0, 10], activeMemoryStarvationMinutes: [1, 1440],
|
|
354
|
+
activeMemoryCooldownMinutes: [0, 10080], activeMemoryRecallCooldownMinutes: [0, 10080],
|
|
346
355
|
}
|
|
347
356
|
const enums = { autoInject: ['smart', 'anchor-only', 'per-turn', 'off'], memoryMode: ['local', 'cloud'], memoryBackend: ['agent', 'api'], vectorSource: ['local', 'api'], newSessionDefault: ['off', 'auto', 'global', 'both'] }
|
|
348
357
|
for (const [key, value] of Object.entries(patch || {})) {
|
|
@@ -541,13 +550,20 @@ export function deleteLibrary(id) {
|
|
|
541
550
|
const state = loadState()
|
|
542
551
|
if (!state.libraries[id]) return { ok: false, error: 'library not found' }
|
|
543
552
|
if (state.libraries[id].archived !== true) return { ok: false, error: 'archive the library before deleting it' }
|
|
553
|
+
const activeVectorDb = removeActiveLibrary(id)
|
|
554
|
+
const legacyVectorDb = path.join(librariesDir(), id + '.sqlite3')
|
|
544
555
|
delete state.libraries[id]
|
|
545
556
|
for (const [sessionId, binding] of Object.entries(state.sessions || {})) {
|
|
546
557
|
if (binding?.libraryId === id) delete state.sessions[sessionId]
|
|
547
558
|
}
|
|
548
559
|
saveState(state)
|
|
549
560
|
const removed = []
|
|
550
|
-
|
|
561
|
+
const vectorPrefix = id + '.'
|
|
562
|
+
const vectorGenerationFiles = fs.existsSync(librariesDir())
|
|
563
|
+
? fs.readdirSync(librariesDir()).filter(name => name.startsWith(vectorPrefix) && name.endsWith('.sqlite3')).map(name => path.join(librariesDir(), name))
|
|
564
|
+
: []
|
|
565
|
+
for (const file of [activeVectorDb, legacyVectorDb, ...vectorGenerationFiles, ...['json', 'entities.json', 'media.json', 'stats.json'].map(suffix => path.join(indexDir(), `${id}.${suffix}`))]) {
|
|
566
|
+
if (!file) continue
|
|
551
567
|
try {
|
|
552
568
|
if (fs.existsSync(file)) { fs.rmSync(file, { force: true }); removed.push(file) }
|
|
553
569
|
} catch {}
|
|
@@ -791,6 +807,8 @@ export function markInjected(sessionId, ids, turn) {
|
|
|
791
807
|
for (const id of ids || []) at[id] = turn ?? Date.now()
|
|
792
808
|
binding.injectedAt = at
|
|
793
809
|
binding.lastInjectedTurn = turn ?? binding.lastInjectedTurn ?? 0
|
|
810
|
+
// 主动记忆抑制要一个"最近一次真正注入了记忆"的墙钟时间;turn 号不能跨会话比较。
|
|
811
|
+
binding.lastInjectedAt = stamp()
|
|
794
812
|
return state
|
|
795
813
|
})
|
|
796
814
|
}
|
|
@@ -870,17 +888,16 @@ export function consumeCue(sessionId, cueId) {
|
|
|
870
888
|
return true
|
|
871
889
|
}
|
|
872
890
|
|
|
873
|
-
// The session's running mood
|
|
874
|
-
//
|
|
875
|
-
// as a stored memory.
|
|
891
|
+
// The session's running mood is a retrieval lens on the binding, not a fact about
|
|
892
|
+
// the user: it must never be recalled as a stored memory.
|
|
876
893
|
export function setSessionMood(sessionId, mood) {
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
})
|
|
894
|
+
// 给了 emotion 就以四轴为准,valence 只是投影;只有 valence 的旧形状原样存,不反推四轴。
|
|
895
|
+
const emotion = normalizeEmotion(mood?.emotion ?? null)
|
|
896
|
+
const bounded = (raw, min, max) => Math.max(min, Math.min(max, Number.isFinite(raw) ? raw : 0)) // 显式 0 是读数的一部分,不能用 `|| 默认值`
|
|
897
|
+
return setSessionBinding(sessionId, { mood: {
|
|
898
|
+
valence: emotion ? derivedValence(emotion) : bounded(mood?.valence, -1, 1),
|
|
899
|
+
emotion, arousal: bounded(mood?.arousal, 0, 1), dominance: bounded(mood?.dominance, -1, 1), at: Date.now(),
|
|
900
|
+
} })
|
|
884
901
|
}
|
|
885
902
|
|
|
886
903
|
export function getSessionMood(sessionId) {
|
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)
|
|
@@ -187,8 +212,16 @@ export function registerTools(ctx) {
|
|
|
187
212
|
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
213
|
},
|
|
189
214
|
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: '
|
|
215
|
+
mood_valence: { type: 'number', description: '派生值:给了 emotion 时由四项算出,-1(坏)到 1(好)。省略则沿用当前会话读数。' },
|
|
191
216
|
arousal: { type: 'number', description: 'How strongly it hit, 0-1.' },
|
|
217
|
+
emotion: {
|
|
218
|
+
type: 'object',
|
|
219
|
+
properties: { joy: { type: 'number' }, anger: { type: 'number' }, sorrow: { type: 'number' }, fear: { type: 'number' } },
|
|
220
|
+
// 四个轴是封闭集合:多写的键只会被误当成第五种情绪。
|
|
221
|
+
additionalProperties: false,
|
|
222
|
+
description: '情绪构成:喜悦/愤怒/哀伤/恐惧,四项自动归一化到和为 1。填记忆所述那件事的情绪,不是此刻整理时的心情。省略=继承当前会话读数;显式全零=明确中性;工程事实请用中性,不要因为 bug 难修或结论重要就填负面情绪。',
|
|
223
|
+
},
|
|
224
|
+
dominance: { type: 'number', description: '谁在掌控:-1 完全失控 … +1 完全掌控。省略=继承当前会话。' },
|
|
192
225
|
peak: { type: 'boolean', description: 'A high point or low point of the arc — peaks and endings stay easier to recall.' },
|
|
193
226
|
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
227
|
track: { type: 'string', description: 'memory (default) or habit for an aggregated expression/sticker habit; do not write one habit per occurrence.' },
|
|
@@ -219,6 +252,11 @@ export function registerTools(ctx) {
|
|
|
219
252
|
confidence: args.confidence,
|
|
220
253
|
moodValence: args.mood_valence ?? null,
|
|
221
254
|
arousal: args.arousal ?? null,
|
|
255
|
+
// `undefined` means "the model said nothing" and is allowed to inherit the
|
|
256
|
+
// turn's mood; `null` means "explicitly neutral" and must survive as such. Using
|
|
257
|
+
// `?? null` here would erase that difference and re-inherit on a neutral call.
|
|
258
|
+
emotion: args.emotion,
|
|
259
|
+
dominance: args.dominance ?? null,
|
|
222
260
|
peak: args.peak === true,
|
|
223
261
|
salientDate: args.salient_date ?? null,
|
|
224
262
|
})
|
|
@@ -309,6 +347,13 @@ export function registerTools(ctx) {
|
|
|
309
347
|
peak: { type: 'boolean', description: 'Flashbulb flag: the moment was charged. Changing it recomputes accuracy.' },
|
|
310
348
|
mood_valence: { type: 'number', description: 'How the moment felt, -1..1. Used as a retrieval lens; omitted keeps the stored value.' },
|
|
311
349
|
arousal: { type: 'number', description: 'How charged the moment was, 0..1. Used as a retrieval lens; omitted keeps the stored value.' },
|
|
350
|
+
emotion: {
|
|
351
|
+
type: 'object',
|
|
352
|
+
properties: { joy: { type: 'number' }, anger: { type: 'number' }, sorrow: { type: 'number' }, fear: { type: 'number' } },
|
|
353
|
+
additionalProperties: false,
|
|
354
|
+
description: '情绪构成,三项语义不同:省略=保留原值;显式 null=清除 emotion,来源变 unknown;显式对象=校验并归一化后整份替换。改的是那条记忆所述事件的感受,不是此刻的心情。',
|
|
355
|
+
},
|
|
356
|
+
dominance: { type: 'number', description: '谁在掌控:-1 完全失控 … +1 完全掌控。省略=保留原值。' },
|
|
312
357
|
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
358
|
},
|
|
314
359
|
async (args, exec) => {
|
|
@@ -318,6 +363,8 @@ export function registerTools(ctx) {
|
|
|
318
363
|
layer: args.layer, owner: args.owner, visibility: args.visibility,
|
|
319
364
|
source: args.source, track: args.track, peak: args.peak,
|
|
320
365
|
moodValence: args.mood_valence, arousal: args.arousal,
|
|
366
|
+
// undefined 就是"省略",必须原样传下去;换成 null 会把保留变成清除。
|
|
367
|
+
emotion: args.emotion, dominance: args.dominance,
|
|
321
368
|
// Sentinel: absent means "leave the date as it is", '' means "clear it".
|
|
322
369
|
salientDate: args.salient_date,
|
|
323
370
|
})
|
package/lib/vector-profile.js
CHANGED
|
@@ -565,6 +565,33 @@ export function dbPathFor(libraryId = null) {
|
|
|
565
565
|
* loadIndex must be synchronous; by default the authoritative raw index is read.
|
|
566
566
|
* A nonempty, corrupt, quarantined or unknown-shaped index requires rebuilding.
|
|
567
567
|
*/
|
|
568
|
+
/**
|
|
569
|
+
* Remove one library from the active database map under the manifest lock.
|
|
570
|
+
* The caller owns filesystem cleanup; returning the captured absolute path keeps
|
|
571
|
+
* the manifest mutation atomic without deleting a possibly newer generation.
|
|
572
|
+
*/
|
|
573
|
+
export function removeActiveLibrary(libraryId) {
|
|
574
|
+
if (!isSafeId(libraryId) || libraryId === 'global') throw vectorError(VECTOR_ERROR_CODES.PATH_ESCAPE, 'unsafe private library id: ' + JSON.stringify(libraryId))
|
|
575
|
+
let removed = null
|
|
576
|
+
updateVectorStateSync(state => {
|
|
577
|
+
const guard = liveGuard(state.writeGuard)
|
|
578
|
+
if (guard.frozen || Object.keys(guard.leases).length) {
|
|
579
|
+
throw vectorError('VECTOR_WRITE_BUSY', 'an in-flight vector write prevents deleting a library; retry shortly', { libraryId })
|
|
580
|
+
}
|
|
581
|
+
if (state.pending) {
|
|
582
|
+
throw vectorError('VECTOR_WRITE_BUSY', 'a vector generation switch prevents deleting a library; retry shortly', { libraryId })
|
|
583
|
+
}
|
|
584
|
+
const active = state.active
|
|
585
|
+
const entry = active?.databases?.libraries?.[libraryId]
|
|
586
|
+
if (!active || !entry) return null
|
|
587
|
+
removed = resolveVectorPath(entry.path)
|
|
588
|
+
const libraries = { ...active.databases.libraries }
|
|
589
|
+
delete libraries[libraryId]
|
|
590
|
+
return { ...state, active: { ...active, databases: { ...active.databases, libraries } } }
|
|
591
|
+
})
|
|
592
|
+
return removed
|
|
593
|
+
}
|
|
594
|
+
|
|
568
595
|
export function enrollActiveLibrary(libraryId, { loadIndex } = {}) {
|
|
569
596
|
if (!isSafeId(libraryId) || libraryId === 'global') throw vectorError(VECTOR_ERROR_CODES.PATH_ESCAPE, 'unsafe private library id: ' + JSON.stringify(libraryId))
|
|
570
597
|
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.
|
|
4
|
+
"version": "1.2.0",
|
|
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",
|
|
@@ -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')
|