dsh-memu-memory 1.1.2 → 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 +8 -6
- package/README.md +50 -5
- package/README.zh.md +49 -5
- package/docs/active-memory-ab.md +385 -0
- package/docs/binding-lifecycle.md +99 -0
- package/docs/contributing.md +6 -4
- package/docs/emotion-model-spec.md +281 -0
- package/docs/env-and-commands.md +13 -0
- package/docs/host-api.md +35 -3
- package/docs/memory-semantics.md +40 -11
- package/docs/recall-benchmark.md +238 -0
- package/docs/troubleshooting.md +395 -366
- package/lib/active-memory.js +165 -0
- package/lib/activity-log.js +190 -0
- package/lib/backup.js +1103 -0
- package/lib/binding-diagnostics.js +635 -0
- package/lib/client.js +728 -58
- 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/curator.js +81 -37
- package/lib/cycle.js +43 -6
- package/lib/embedding-catalog.js +763 -0
- package/lib/embedding-download.js +407 -0
- package/lib/embedding-runtime.js +710 -0
- package/lib/engine.js +2 -2
- package/lib/index.js +36 -0
- package/lib/inject.js +232 -103
- package/lib/local-embedding-entry.js +145 -0
- package/lib/local-embedding-manager.js +267 -0
- package/lib/local-embedding-protocol.js +173 -0
- package/lib/local-embedding-worker.js +652 -0
- package/lib/local-vector-service.js +283 -0
- package/lib/memory.js +599 -57
- package/lib/memu-config.js +10 -22
- package/lib/memu.js +53 -19
- package/lib/mood.js +268 -39
- package/lib/paths.js +24 -0
- package/lib/preset-scope.js +1 -1
- package/lib/private.js +450 -29
- 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 +137 -172
- package/lib/tools.js +58 -7
- package/lib/vector-migration.js +878 -0
- package/lib/vector-profile.js +968 -0
- package/package.json +4 -2
- package/scripts/acceptance-local.mjs +7 -5
- package/scripts/active-memory-ab-live.mjs +655 -0
- package/scripts/active-memory-ab.mjs +1302 -0
- package/scripts/chain-matrix.mjs +537 -0
- package/scripts/check-all.mjs +29 -1
- package/scripts/check-doc-drift.mjs +353 -0
- package/scripts/diagnose-bindings.mjs +176 -0
- package/scripts/local-runtime-install.mjs +142 -0
- package/scripts/local-runtime-verify.mjs +56 -0
- package/scripts/local-vector-verify.mjs +93 -0
- package/scripts/recall-bench.mjs +813 -0
- package/scripts/repetition-ab-scenarios.mjs +93 -0
- package/scripts/repetition-detector-snapshot.mjs +314 -0
- package/scripts/repetition-eval-live.mjs +360 -0
- package/scripts/repetition-eval.mjs +702 -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 +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/state.js
CHANGED
|
@@ -5,12 +5,21 @@ import {
|
|
|
5
5
|
dataDir, librariesDir, indexDir, backupsDir,
|
|
6
6
|
ensureDir, readJson, readFailure, readFailuresUnder, writeJson, withFileLock, assertJsonShape, warn, stamp, shortHash, libraryIdForScope, isSafeId,
|
|
7
7
|
} from './paths.js'
|
|
8
|
+
import {
|
|
9
|
+
activeLeases, createBackup, deleteBackupEntry, listBackupEntries,
|
|
10
|
+
pruneBackups, restoreFromBackup, withBackupLock,
|
|
11
|
+
} from './backup.js'
|
|
8
12
|
import { migrateIndexVisibility } from './visibility.js'
|
|
13
|
+
import { memuDbFor, memuGlobalDbFor, removeActiveLibrary } from './vector-profile.js'
|
|
14
|
+
import { normalizeEmotion, derivedValence } from './mood.js'
|
|
9
15
|
|
|
10
16
|
const STATE_FILE = () => path.join(dataDir(), 'state.json')
|
|
11
17
|
const GLOBAL_DB = () => path.join(dataDir(), 'global.sqlite3')
|
|
12
18
|
const GLOBAL_INDEX = () => path.join(indexDir(), 'global.json')
|
|
13
19
|
|
|
20
|
+
// Observable outcome of the last backup action; see listBackupEntries().
|
|
21
|
+
let lastBackupReport = null
|
|
22
|
+
|
|
14
23
|
export const DEFAULT_SETTINGS = {
|
|
15
24
|
sidebarSettings: false,
|
|
16
25
|
visionCaptionEnabled: true,
|
|
@@ -24,12 +33,17 @@ export const DEFAULT_SETTINGS = {
|
|
|
24
33
|
// "too little memory", it is noise: a per-turn block sized for paragraph-long
|
|
25
34
|
// prompts drowns the conversation it was meant to help. So the per-turn budgets
|
|
26
35
|
// are small, and the windows that stop repetition are measured in many turns.
|
|
27
|
-
privateTopK:
|
|
28
|
-
globalTopK:
|
|
29
|
-
anchorTokens:
|
|
30
|
-
//
|
|
31
|
-
// so the injected block has to stay clearly smaller than the message it
|
|
32
|
-
|
|
36
|
+
privateTopK: 5,
|
|
37
|
+
globalTopK: 5,
|
|
38
|
+
anchorTokens: 800,
|
|
39
|
+
// 800 tokens for one recall block. A dense chat calls recall on almost every
|
|
40
|
+
// message, so the injected block has to stay clearly smaller than the message it
|
|
41
|
+
// serves. This is the recall budget, not the whole plugin-message ceiling.
|
|
42
|
+
turnTokens: 800,
|
|
43
|
+
// Whole plugin-message ceiling: anchor + recall + optional cue/texture. It must
|
|
44
|
+
// be at least the sum of the sub-budgets the user wants to allow; the default leaves
|
|
45
|
+
// room for the default anchor and recall blocks.
|
|
46
|
+
injectionTotalTokens: 2000,
|
|
33
47
|
relevanceThreshold: 0.4,
|
|
34
48
|
// 16 turns is a couple of minutes of group traffic. The old 8 expired inside a
|
|
35
49
|
// single burst, so the same entry came back every minute or two.
|
|
@@ -81,6 +95,12 @@ export const DEFAULT_SETTINGS = {
|
|
|
81
95
|
// "gone", and short enough that a conversation which ended gets tidied in the
|
|
82
96
|
// same sitting instead of waiting for a quiet hour that may never arrive.
|
|
83
97
|
sleepAfterIdleMinutes: 3,
|
|
98
|
+
// 主动记忆:分数门槛、饥饿阈值(空闲超过它无条件检查)、两种抑制冷却(分钟;整理冷却默认 0,
|
|
99
|
+
// 薄证据的重复 fork 由评分拦截,只有用户显式配置才启用硬冷却)。
|
|
100
|
+
activeMemoryMinScore: 0.8,
|
|
101
|
+
activeMemoryStarvationMinutes: 15,
|
|
102
|
+
activeMemoryCooldownMinutes: 0,
|
|
103
|
+
activeMemoryRecallCooldownMinutes: 15,
|
|
84
104
|
autoCurateEveryHours: 0,
|
|
85
105
|
autoCurateSessionLimit: 8,
|
|
86
106
|
// 'local' keeps every memory on this machine; 'cloud' hands MemU's own backend
|
|
@@ -90,6 +110,7 @@ export const DEFAULT_SETTINGS = {
|
|
|
90
110
|
// agent writes and summarizes into the JSON index, with no MemU engine and no
|
|
91
111
|
// embedding API. 'api' hands the work to the configured remote MemU service.
|
|
92
112
|
memoryBackend: 'agent',
|
|
113
|
+
vectorSource: 'local',
|
|
93
114
|
habitsInjection: true,
|
|
94
115
|
// A positive age marks cleanup candidates; deletion always needs a user action.
|
|
95
116
|
purgeArchivedAfterDays: 0,
|
|
@@ -110,10 +131,14 @@ function defaultState() {
|
|
|
110
131
|
function mergeState(raw) {
|
|
111
132
|
const base = defaultState()
|
|
112
133
|
if (!raw || typeof raw !== 'object') return base
|
|
134
|
+
const rawSettings = raw.settings && typeof raw.settings === 'object' ? raw.settings : {}
|
|
135
|
+
const legacyVectorSource = rawSettings.vectorSource === 'api' || rawSettings.vectorSource === 'local'
|
|
136
|
+
? rawSettings.vectorSource
|
|
137
|
+
: (rawSettings.memoryMode === 'cloud' ? 'api' : 'local')
|
|
113
138
|
return {
|
|
114
139
|
...base,
|
|
115
140
|
...raw,
|
|
116
|
-
settings: { ...base.settings, ...
|
|
141
|
+
settings: { ...base.settings, ...rawSettings, vectorSource: legacyVectorSource },
|
|
117
142
|
libraries: raw.libraries && typeof raw.libraries === 'object' ? raw.libraries : {},
|
|
118
143
|
sessions: raw.sessions && typeof raw.sessions === 'object' ? raw.sessions : {},
|
|
119
144
|
workspacePolicies: raw.workspacePolicies && typeof raw.workspacePolicies === 'object' ? raw.workspacePolicies : {},
|
|
@@ -306,16 +331,29 @@ export function getMemoryBackend() {
|
|
|
306
331
|
return loadState().settings?.memoryBackend === 'api' ? 'api' : 'agent'
|
|
307
332
|
}
|
|
308
333
|
|
|
334
|
+
// Single route authority for automatic memory reads and writes.
|
|
335
|
+
export function effectiveVectorRoute(settings = loadState().settings) {
|
|
336
|
+
if (settings?.memoryBackend !== 'api') return 'lexical'
|
|
337
|
+
return settings?.vectorSource === 'api' ? 'remote-api' : 'managed-local'
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
export function vectorBackendEnabled(settings = loadState().settings) {
|
|
341
|
+
return effectiveVectorRoute(settings) !== 'lexical'
|
|
342
|
+
}
|
|
343
|
+
|
|
309
344
|
export function updateSettings(patch) {
|
|
310
345
|
const clean = {}
|
|
311
346
|
const ranges = {
|
|
312
347
|
privateTopK: [1, 20], globalTopK: [1, 20], anchorTokens: [160, 8000], turnTokens: [80, 8000],
|
|
348
|
+
injectionTotalTokens: [160, 16000],
|
|
313
349
|
relevanceThreshold: [0, 1], dedupeTurns: [0, 200], globalEveryNTurns: [0, 200], minUserChars: [0, 1000],
|
|
314
350
|
mustRememberCap: [1, 100], randomCueChance: [0, 1], shortTermTtlHours: [0, 8760],
|
|
315
351
|
unpinBudgetPerSession: [1, 20], unpinCooldownMinutes: [0, 1440], sleepAfterIdleMinutes: [1, 1440],
|
|
316
352
|
autoCurateEveryHours: [0, 8760], autoCurateSessionLimit: [1, 10], purgeArchivedAfterDays: [0, 36500],
|
|
353
|
+
activeMemoryMinScore: [0, 10], activeMemoryStarvationMinutes: [1, 1440],
|
|
354
|
+
activeMemoryCooldownMinutes: [0, 10080], activeMemoryRecallCooldownMinutes: [0, 10080],
|
|
317
355
|
}
|
|
318
|
-
const enums = { autoInject: ['smart', 'anchor-only', 'per-turn', 'off'], memoryMode: ['local', 'cloud'], memoryBackend: ['agent', 'api'], newSessionDefault: ['off', 'auto', 'global', 'both'] }
|
|
356
|
+
const enums = { autoInject: ['smart', 'anchor-only', 'per-turn', 'off'], memoryMode: ['local', 'cloud'], memoryBackend: ['agent', 'api'], vectorSource: ['local', 'api'], newSessionDefault: ['off', 'auto', 'global', 'both'] }
|
|
319
357
|
for (const [key, value] of Object.entries(patch || {})) {
|
|
320
358
|
if (!Object.hasOwn(DEFAULT_SETTINGS, key)) continue
|
|
321
359
|
if (enums[key]) {
|
|
@@ -512,13 +550,20 @@ export function deleteLibrary(id) {
|
|
|
512
550
|
const state = loadState()
|
|
513
551
|
if (!state.libraries[id]) return { ok: false, error: 'library not found' }
|
|
514
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')
|
|
515
555
|
delete state.libraries[id]
|
|
516
556
|
for (const [sessionId, binding] of Object.entries(state.sessions || {})) {
|
|
517
557
|
if (binding?.libraryId === id) delete state.sessions[sessionId]
|
|
518
558
|
}
|
|
519
559
|
saveState(state)
|
|
520
560
|
const removed = []
|
|
521
|
-
|
|
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
|
|
522
567
|
try {
|
|
523
568
|
if (fs.existsSync(file)) { fs.rmSync(file, { force: true }); removed.push(file) }
|
|
524
569
|
} catch {}
|
|
@@ -762,6 +807,8 @@ export function markInjected(sessionId, ids, turn) {
|
|
|
762
807
|
for (const id of ids || []) at[id] = turn ?? Date.now()
|
|
763
808
|
binding.injectedAt = at
|
|
764
809
|
binding.lastInjectedTurn = turn ?? binding.lastInjectedTurn ?? 0
|
|
810
|
+
// 主动记忆抑制要一个"最近一次真正注入了记忆"的墙钟时间;turn 号不能跨会话比较。
|
|
811
|
+
binding.lastInjectedAt = stamp()
|
|
765
812
|
return state
|
|
766
813
|
})
|
|
767
814
|
}
|
|
@@ -841,17 +888,16 @@ export function consumeCue(sessionId, cueId) {
|
|
|
841
888
|
return true
|
|
842
889
|
}
|
|
843
890
|
|
|
844
|
-
// The session's running mood
|
|
845
|
-
//
|
|
846
|
-
// 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.
|
|
847
893
|
export function setSessionMood(sessionId, mood) {
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
})
|
|
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
|
+
} })
|
|
855
901
|
}
|
|
856
902
|
|
|
857
903
|
export function getSessionMood(sessionId) {
|
|
@@ -906,12 +952,12 @@ export function markAnniversaryShown(sessionId, memoryId, dayKey) {
|
|
|
906
952
|
|
|
907
953
|
export function memoryDbPath(libraryId) {
|
|
908
954
|
ensureDir(librariesDir())
|
|
909
|
-
return path.join(librariesDir(), `${libraryId}.sqlite3`)
|
|
955
|
+
return effectiveVectorRoute() === 'managed-local' ? memuDbFor(libraryId) : path.join(librariesDir(), `${libraryId}.sqlite3`)
|
|
910
956
|
}
|
|
911
957
|
|
|
912
958
|
export function globalDbPath() {
|
|
913
959
|
ensureDir(dataDir())
|
|
914
|
-
return GLOBAL_DB()
|
|
960
|
+
return effectiveVectorRoute() === 'managed-local' ? memuGlobalDbFor() : GLOBAL_DB()
|
|
915
961
|
}
|
|
916
962
|
|
|
917
963
|
// Index files are parsed on every folder in the sidebar, and the parse (full
|
|
@@ -1144,175 +1190,94 @@ export function newMemoryId(prefix = 'mem') {
|
|
|
1144
1190
|
return `${prefix}-${Date.now().toString(36)}-${crypto.randomBytes(3).toString('hex')}`
|
|
1145
1191
|
}
|
|
1146
1192
|
|
|
1147
|
-
|
|
1193
|
+
/**
|
|
1194
|
+
* Creates a managed backup of the local memory stores (plugin state, indexes,
|
|
1195
|
+
* prompts, attachments, the vector journal and every MemU database), then keeps
|
|
1196
|
+
* the newest BACKUP_KEEP complete ones. Retention, staging, checksums, atomic
|
|
1197
|
+
* publish and the shared lock all live in lib/backup.js so there is exactly one
|
|
1198
|
+
* place that decides what a backup is and which backup may be deleted.
|
|
1199
|
+
*
|
|
1200
|
+
* Returns the published directory: callers (engine switch, library delete,
|
|
1201
|
+
* scripts) treat the return value as a path.
|
|
1202
|
+
*/
|
|
1203
|
+
export function backupDatabases(options = {}) {
|
|
1148
1204
|
// Pending bookkeeping changes live in memory for up to 50ms; a backup taken
|
|
1149
1205
|
// before a flush would silently capture the previous state.
|
|
1150
|
-
|
|
1151
|
-
const dest = path.join(backupsDir(), stamp().replace(/[:.]/g, '-'))
|
|
1152
|
-
ensureDir(dest)
|
|
1153
|
-
ensureDir(path.join(dest, 'libraries'))
|
|
1154
|
-
ensureDir(path.join(dest, 'index'))
|
|
1155
|
-
// The prompt manifest copy below silently no-opped without this directory —
|
|
1156
|
-
// an empty catch swallowed the ENOENT and backups shipped without it.
|
|
1157
|
-
ensureDir(path.join(dest, 'prompts'))
|
|
1158
|
-
const copy = (from, to) => {
|
|
1159
|
-
let info
|
|
1160
|
-
try { info = fs.lstatSync(from) } catch (error) { if (error.code === 'ENOENT') return; throw error }
|
|
1161
|
-
if (!info.isFile()) throw new Error(`backup refuses non-file: ${path.basename(from)}`)
|
|
1162
|
-
fs.copyFileSync(from, to)
|
|
1163
|
-
}
|
|
1164
|
-
copy(STATE_FILE(), path.join(dest, 'state.json'))
|
|
1165
|
-
copy(GLOBAL_DB(), path.join(dest, 'global.sqlite3'))
|
|
1166
|
-
copy(GLOBAL_INDEX(), path.join(dest, 'index', 'global.json'))
|
|
1167
|
-
copy(path.join(dataDir(), 'prompts', 'manifest.json'), path.join(dest, 'prompts', 'manifest.json'))
|
|
1168
|
-
try {
|
|
1169
|
-
for (const name of fs.readdirSync(librariesDir())) {
|
|
1170
|
-
if (name.endsWith('.sqlite3') || name.endsWith('.sqlite3-wal') || name.endsWith('.sqlite3-shm')) {
|
|
1171
|
-
copy(path.join(librariesDir(), name), path.join(dest, 'libraries', name))
|
|
1172
|
-
}
|
|
1173
|
-
}
|
|
1174
|
-
} catch (error) { if (error.code !== 'ENOENT') throw error }
|
|
1206
|
+
let report
|
|
1175
1207
|
try {
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
const walk = (dir, prefix = '') => {
|
|
1197
|
-
let names = []
|
|
1198
|
-
try { names = fs.readdirSync(dir) } catch { return }
|
|
1199
|
-
for (const name of names) {
|
|
1200
|
-
const full = path.join(dir, name)
|
|
1201
|
-
let stat = null
|
|
1202
|
-
try { stat = fs.statSync(full) } catch { continue }
|
|
1203
|
-
if (stat.isDirectory()) { walk(full, `${prefix}${name}/`); continue }
|
|
1204
|
-
let sha256 = null
|
|
1205
|
-
try { sha256 = crypto.createHash('sha256').update(fs.readFileSync(full)).digest('hex') } catch {}
|
|
1206
|
-
files[`${prefix}${name}`] = { bytes: stat.size, sha256 }
|
|
1208
|
+
report = createBackup({ ...options, flushState })
|
|
1209
|
+
} catch (error) {
|
|
1210
|
+
// A refusal is the safety property working, not something to swallow: a
|
|
1211
|
+
// database the engine could not snapshot aborts the backup, and the reason
|
|
1212
|
+
// has to stay visible on the same channel the panel already reads.
|
|
1213
|
+
lastBackupReport = {
|
|
1214
|
+
at: stamp(),
|
|
1215
|
+
action: 'create',
|
|
1216
|
+
status: 'failed',
|
|
1217
|
+
code: error?.code || null,
|
|
1218
|
+
error: String(error?.message || error),
|
|
1219
|
+
name: null,
|
|
1220
|
+
dir: null,
|
|
1221
|
+
files: 0,
|
|
1222
|
+
bytes: 0,
|
|
1223
|
+
snapshots: [],
|
|
1224
|
+
pruned: [],
|
|
1225
|
+
pruneFailed: [],
|
|
1226
|
+
pendingCleanup: 0,
|
|
1227
|
+
notice: null,
|
|
1207
1228
|
}
|
|
1229
|
+
throw error
|
|
1208
1230
|
}
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
writeJson(path.join(dest, 'manifest.json'), {
|
|
1212
|
-
version: 1,
|
|
1213
|
-
createdAt: stamp(),
|
|
1214
|
-
engine: current ? { version: current.version, schemaVersion: current.schemaVersion || null } : null,
|
|
1215
|
-
files,
|
|
1216
|
-
})
|
|
1217
|
-
return dest
|
|
1231
|
+
lastBackupReport = { at: stamp(), action: 'create', ...report }
|
|
1232
|
+
return report.dir
|
|
1218
1233
|
}
|
|
1219
1234
|
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
} catch {
|
|
1224
|
-
return null
|
|
1225
|
-
}
|
|
1235
|
+
/** What the last backup/restore/prune did, so the outcome stays observable. */
|
|
1236
|
+
export function lastBackupReportView() {
|
|
1237
|
+
return lastBackupReport ? { ...lastBackupReport } : null
|
|
1226
1238
|
}
|
|
1227
1239
|
|
|
1240
|
+
/** Newest first, including entries that failed to complete (reported, never deleted). */
|
|
1228
1241
|
export function listBackups() {
|
|
1229
|
-
|
|
1230
|
-
try { names = fs.readdirSync(backupsDir()) } catch { return [] }
|
|
1231
|
-
return names
|
|
1232
|
-
.map((name) => {
|
|
1233
|
-
const dir = path.join(backupsDir(), name)
|
|
1234
|
-
let stat = null
|
|
1235
|
-
try { stat = fs.statSync(dir) } catch { return null }
|
|
1236
|
-
if (!stat.isDirectory()) return null
|
|
1237
|
-
const manifest = readJson(path.join(dir, 'manifest.json'), null)
|
|
1238
|
-
const fileCount = manifest?.files ? Object.keys(manifest.files).length : null
|
|
1239
|
-
const bytes = manifest?.files
|
|
1240
|
-
? Object.values(manifest.files).reduce((sum, f) => sum + Number(f.bytes || 0), 0)
|
|
1241
|
-
: null
|
|
1242
|
-
return {
|
|
1243
|
-
name,
|
|
1244
|
-
dir,
|
|
1245
|
-
createdAt: manifest?.createdAt || stat.mtime.toISOString(),
|
|
1246
|
-
engine: manifest?.engine || null,
|
|
1247
|
-
files: fileCount,
|
|
1248
|
-
bytes,
|
|
1249
|
-
hasManifest: Boolean(manifest),
|
|
1250
|
-
}
|
|
1251
|
-
})
|
|
1252
|
-
.filter(Boolean)
|
|
1253
|
-
.sort((a, b) => String(b.name).localeCompare(String(a.name)))
|
|
1242
|
+
return listBackupEntries()
|
|
1254
1243
|
}
|
|
1255
1244
|
|
|
1256
1245
|
/** Removes one backup directory. The name is validated, never used as a path. */
|
|
1257
1246
|
export function deleteBackup(name) {
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
fs.rmSync(dir, { recursive: true, force: true })
|
|
1262
|
-
return { deleted: name }
|
|
1247
|
+
const result = deleteBackupEntry(name)
|
|
1248
|
+
lastBackupReport = { at: stamp(), action: 'delete', ...result }
|
|
1249
|
+
return { deleted: result.deleted, status: result.status }
|
|
1263
1250
|
}
|
|
1264
1251
|
|
|
1265
1252
|
/**
|
|
1266
|
-
*
|
|
1267
|
-
*
|
|
1268
|
-
*
|
|
1253
|
+
* Applies the retention rule on demand. Creation already prunes, so this exists
|
|
1254
|
+
* for the paths that must reclaim the space a protected restore target kept
|
|
1255
|
+
* alive, and for callers that want the report without making a backup.
|
|
1269
1256
|
*/
|
|
1270
|
-
export function
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
}
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
copy(path.join(dir, 'index', 'global.json'), GLOBAL_INDEX())
|
|
1295
|
-
copy(path.join(dir, 'prompts', 'manifest.json'), path.join(dataDir(), 'prompts', 'manifest.json'))
|
|
1296
|
-
for (const [sub, target] of [['index', indexDir()], ['libraries', librariesDir()]]) {
|
|
1297
|
-
let names = []
|
|
1298
|
-
try { names = fs.readdirSync(path.join(dir, sub)) } catch { continue }
|
|
1299
|
-
for (const file of names) copy(path.join(dir, sub, file), path.join(target, file))
|
|
1300
|
-
}
|
|
1301
|
-
const restoreTree = (from, target) => {
|
|
1302
|
-
let items
|
|
1303
|
-
try { items = fs.readdirSync(from, { withFileTypes: true }) } catch (error) { if (error.code === 'ENOENT') return; throw error }
|
|
1304
|
-
for (const item of items) {
|
|
1305
|
-
if (item.isSymbolicLink()) throw new Error('restore refuses symbolic links')
|
|
1306
|
-
if (item.isDirectory()) restoreTree(path.join(from, item.name), path.join(target, item.name))
|
|
1307
|
-
else copy(path.join(from, item.name), path.join(target, item.name))
|
|
1308
|
-
}
|
|
1309
|
-
}
|
|
1310
|
-
restoreTree(path.join(dir, 'attachments'), path.join(dataDir(), 'attachments'))
|
|
1311
|
-
restoreTree(path.join(dir, 'prompts'), path.join(dataDir(), 'prompts'))
|
|
1312
|
-
// The restored files are the truth now; anything cached describes the state
|
|
1313
|
-
// that was just replaced.
|
|
1314
|
-
clearCaches()
|
|
1315
|
-
return { restored: name, safety }
|
|
1257
|
+
export function pruneOldBackups(options = {}) {
|
|
1258
|
+
const result = pruneBackups(options)
|
|
1259
|
+
lastBackupReport = { at: stamp(), action: 'prune', ...result }
|
|
1260
|
+
return result
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
/** Names of backups that a running restore or migration is reading right now. */
|
|
1264
|
+
export function leasedBackups() {
|
|
1265
|
+
return Object.keys(activeLeases())
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
/**
|
|
1269
|
+
* Puts a backup back. The target is leased and its whole inventory is verified
|
|
1270
|
+
* before anything is written; the live root is saved as a safety backup next;
|
|
1271
|
+
* only then are files replaced. Failure leaves a recovery path: the safety
|
|
1272
|
+
* backup holds the state that was replaced and retention may not delete it.
|
|
1273
|
+
* A backup that predates the vector journal also retires the stale live
|
|
1274
|
+
* vector-active pointer, so recall falls back to text instead of querying
|
|
1275
|
+
* databases that no longer match the restored index.
|
|
1276
|
+
*/
|
|
1277
|
+
export function restoreBackup(name, { beforeRestore = null, keep = undefined } = {}) {
|
|
1278
|
+
const result = restoreFromBackup(name, { beforeRestore, flushState, clearCaches, keep })
|
|
1279
|
+
lastBackupReport = { at: stamp(), action: 'restore', ...result }
|
|
1280
|
+
return result
|
|
1316
1281
|
}
|
|
1317
1282
|
|
|
1318
1283
|
/** Drops the in-memory mirrors so the next read comes from disk. */
|
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,10 +212,18 @@ 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
|
-
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.' },
|
|
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.' },
|
|
195
228
|
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.' },
|
|
196
229
|
context: { type: 'object', properties: {
|
|
@@ -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
|
})
|
|
@@ -252,11 +290,14 @@ export function registerTools(ctx) {
|
|
|
252
290
|
},
|
|
253
291
|
async (args, exec) => {
|
|
254
292
|
const result = await privateRecall({ exec, query: args.query, topK: args.topK })
|
|
293
|
+
// Surface a degraded semantic leg on the explicit tool path too; silently
|
|
294
|
+
// returning fewer hits would make a failed backend look like an empty library.
|
|
295
|
+
const warning = (result.errors || []).length ? `\n注意:本次召回已降级:${(result.errors || []).join('; ')}` : ''
|
|
255
296
|
// Read-only: never materialize a binding/library from a recall header.
|
|
256
297
|
const binding = peekBinding(exec)
|
|
257
298
|
const header = `私有记忆(library=${binding?.libraryId || '?'}, domain=${domainOf(binding)})`
|
|
258
|
-
if (!result.files?.length) return `${header}\n
|
|
259
|
-
return [header, ...result.files.map((f, i) => formatPrivateItem(f, i + 1, { query: args.query }))].join('\n\n')
|
|
299
|
+
if (!result.files?.length) return `${header}\n没有命中。${warning}`
|
|
300
|
+
return [header, ...result.files.map((f, i) => formatPrivateItem(f, i + 1, { query: args.query }))].join('\n\n') + warning
|
|
260
301
|
},
|
|
261
302
|
{ isConcurrencySafe: () => true },
|
|
262
303
|
))
|
|
@@ -270,14 +311,15 @@ export function registerTools(ctx) {
|
|
|
270
311
|
// instead of ensureSession() silently creating a placeholder library.
|
|
271
312
|
if (!peekBinding(exec)?.libraryId) return '当前对话没有绑定记忆库。'
|
|
272
313
|
const result = await privateList({ exec })
|
|
273
|
-
|
|
314
|
+
const warning = result.warning ? `\n注意:${result.warning}` : ''
|
|
315
|
+
if (!result.files?.length) return `私有记忆库 ${result.libraryId} 为空。${warning}`
|
|
274
316
|
const lines = [`私有记忆库 ${result.libraryId}(${result.files.length} 条)`]
|
|
275
317
|
for (const file of result.files) {
|
|
276
318
|
const e = file.entry || {}
|
|
277
319
|
const tag = salientTag(e)
|
|
278
320
|
lines.push(`- [${file.name}] ${e.title || file.description || '(untitled)'} · ${e.layer || 'long_term'} · ${e.status || 'active'}${e.marginalized ? ' · marginalized' : ''}${tag ? ` · ${tag}` : ''}`)
|
|
279
321
|
}
|
|
280
|
-
return lines.join('\n')
|
|
322
|
+
return lines.join('\n') + warning
|
|
281
323
|
},
|
|
282
324
|
{ isConcurrencySafe: () => true },
|
|
283
325
|
))
|
|
@@ -305,6 +347,13 @@ export function registerTools(ctx) {
|
|
|
305
347
|
peak: { type: 'boolean', description: 'Flashbulb flag: the moment was charged. Changing it recomputes accuracy.' },
|
|
306
348
|
mood_valence: { type: 'number', description: 'How the moment felt, -1..1. Used as a retrieval lens; omitted keeps the stored value.' },
|
|
307
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 完全掌控。省略=保留原值。' },
|
|
308
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.' },
|
|
309
358
|
},
|
|
310
359
|
async (args, exec) => {
|
|
@@ -314,6 +363,8 @@ export function registerTools(ctx) {
|
|
|
314
363
|
layer: args.layer, owner: args.owner, visibility: args.visibility,
|
|
315
364
|
source: args.source, track: args.track, peak: args.peak,
|
|
316
365
|
moodValence: args.mood_valence, arousal: args.arousal,
|
|
366
|
+
// undefined 就是"省略",必须原样传下去;换成 null 会把保留变成清除。
|
|
367
|
+
emotion: args.emotion, dominance: args.dominance,
|
|
317
368
|
// Sentinel: absent means "leave the date as it is", '' means "clear it".
|
|
318
369
|
salientDate: args.salient_date,
|
|
319
370
|
})
|