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/mood.js
CHANGED
|
@@ -1,66 +1,277 @@
|
|
|
1
|
-
// Cheap, local, deterministic
|
|
2
|
-
//
|
|
1
|
+
// Cheap, local, deterministic affect inference. No model call: the plugin needs an
|
|
2
|
+
// affect signal every turn for affect-congruent retrieval, and a round trip per turn
|
|
3
3
|
// would make injection slower than the memory it fetches.
|
|
4
4
|
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
]
|
|
15
|
-
|
|
16
|
-
const AROUSAL_WORDS = [
|
|
17
|
-
[/(!{2,}|[!?]{2,}|太|超|巨|特别|疯狂|爆炸|never|always|everything|nothing)/i, 0.8],
|
|
18
|
-
[/必须|一定|马上|立刻|不能|别|停止|紧急|must|need|stop|urgent|now/i, 0.7],
|
|
19
|
-
[/可能|也许|大概|想想|不确定|maybe|perhaps|might|wonder/i, 0.25],
|
|
20
|
-
]
|
|
5
|
+
// Model: four shares on the simplex (joy / anger / sorrow / fear) + two auxiliary
|
|
6
|
+
// axes — arousal (how loud) and dominance (who is in control). Shares answer "which
|
|
7
|
+
// feeling, in what proportion"; arousal answers "how loud"; dominance answers "who is
|
|
8
|
+
// steering". Normalising the shares throws intensity away, which is exactly why
|
|
9
|
+
// arousal has to stay a separate field rather than being folded into the four.
|
|
10
|
+
//
|
|
11
|
+
// Scores are deliberately coarse. A wrong read only nudges recall ordering; it is
|
|
12
|
+
// never stored as a fact about the user.
|
|
13
|
+
|
|
14
|
+
export const EMOTION_AXES = Object.freeze(['joy', 'anger', 'sorrow', 'fear'])
|
|
21
15
|
|
|
22
16
|
// Mood fades back to neutral on its own, so an old read does not tint the whole
|
|
23
17
|
// day's retrieval.
|
|
24
|
-
const MOOD_HALF_LIFE_MINUTES = 90
|
|
18
|
+
export const MOOD_HALF_LIFE_MINUTES = 90
|
|
25
19
|
// A resting level, not zero: a calm stretch reads as calm-and-alert, which is what
|
|
26
20
|
// keeps the arousal term meaningful when nothing is happening.
|
|
27
21
|
export const BASE_AROUSAL = 0.35
|
|
22
|
+
// Below this share no single axis is dominant enough to call the reading a mood.
|
|
23
|
+
export const EMOTION_MIN_SHARE = 0.35
|
|
24
|
+
// The composition stops being worth anything once the whole reading has faded this
|
|
25
|
+
// far; arousal and dominance keep decaying and are still reported.
|
|
26
|
+
export const EMOTION_FADE_FLOOR = 0.35
|
|
27
|
+
// How much anger-vs-fear moves dominance on its own. Anger carries agency, fear does
|
|
28
|
+
// not, and neither phrase usually contains a control word.
|
|
29
|
+
export const DOMINANCE_PRIOR_WEIGHT = 0.4
|
|
30
|
+
// Every axis projects onto the legacy valence with the same magnitude; only joy is
|
|
31
|
+
// positive. Anger is *not* a mild positive — it pulls the projection down as hard as
|
|
32
|
+
// sorrow does.
|
|
33
|
+
export const VALENCE_SCALE = 0.8
|
|
34
|
+
|
|
35
|
+
const NEGATION_AS_SAD = /不开心|不高兴|不喜欢|不满意|不顺利|not\s+happy|not\s+good|don't\s+like/gi
|
|
36
|
+
const NEGATION_TO_NEUTRAL = /不难过|不害怕|不焦虑|不生气|not\s+sad|not\s+afraid|not\s+anxious/gi
|
|
37
|
+
|
|
38
|
+
const AXIS_PATTERNS = Object.freeze({
|
|
39
|
+
joy: [
|
|
40
|
+
[/开心|高兴|太好了|喜欢|爱你|满意|惊喜|谢谢|感谢|期待|幸福|欣慰|兴奋|激动|雀跃/, 0.9],
|
|
41
|
+
[/不错|还行|顺利|爽|安心|踏实|棒|有趣|舒服/, 0.6],
|
|
42
|
+
],
|
|
43
|
+
anger: [
|
|
44
|
+
[/气死|烦死|受不了|离谱|凭什么|恨|火大|欺人太甚/, 0.9],
|
|
45
|
+
[/生气|气|烦|讨厌|恶心|无语|恼火|不满|angry|annoying|hate|furious/, 0.6],
|
|
46
|
+
],
|
|
47
|
+
sorrow: [
|
|
48
|
+
[/崩溃|难受|心疼|失望透顶|想哭|哭|心碎/, 0.9],
|
|
49
|
+
// `sad` is not decoration: the negation rules below rewrite 不开心/不高兴 into the
|
|
50
|
+
// word `sad`, so dropping English here silently turns a negated positive into a
|
|
51
|
+
// neutral reading instead of a negative one.
|
|
52
|
+
[/难过|失望|委屈|郁闷|累|困|没意思|算了|沮丧|孤独|sad|unhappy|upset|tired|miserable/, 0.6],
|
|
53
|
+
],
|
|
54
|
+
fear: [
|
|
55
|
+
[/吓死|害怕|恐慌|焦虑|绝望|惊恐|发抖/, 0.9],
|
|
56
|
+
[/担心|紧张|慌|不敢|万一|怕|不安|忐忑|afraid|scared|anxious|worried|nervous/, 0.6],
|
|
57
|
+
],
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
// Compound expressions are mixtures, not new axes. Each entry is a named point on the
|
|
61
|
+
// simplex plus the dominance that usually comes with it.
|
|
62
|
+
export const COMPOUND_PRESETS = Object.freeze({
|
|
63
|
+
委屈: Object.freeze({ emotion: Object.freeze({ joy: 0, anger: 0.3, sorrow: 0.7, fear: 0 }), dominance: -0.4 }),
|
|
64
|
+
尴尬: Object.freeze({ emotion: Object.freeze({ joy: 0, anger: 0, sorrow: 0.5, fear: 0.5 }), dominance: -0.5 }),
|
|
65
|
+
嫉妒: Object.freeze({ emotion: Object.freeze({ joy: 0, anger: 0.5, sorrow: 0.3, fear: 0.2 }), dominance: -0.1 }),
|
|
66
|
+
心疼: Object.freeze({ emotion: Object.freeze({ joy: 0.2, anger: 0, sorrow: 0.8, fear: 0 }), dominance: -0.2 }),
|
|
67
|
+
不甘: Object.freeze({ emotion: Object.freeze({ joy: 0, anger: 0.6, sorrow: 0.4, fear: 0 }), dominance: 0.4 }),
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
const COMPOUND_PATTERN = new RegExp(Object.keys(COMPOUND_PRESETS).join('|'))
|
|
71
|
+
const HEDGE_PATTERN = /有点|稍微|一点点|a bit|somewhat/i
|
|
72
|
+
// "一点都不好" is an intensifier, not a hedge: it must never damp what follows.
|
|
73
|
+
const HEDGE_SKEW_GUARD = /一点都|一点也不|一点也没|一点没有/
|
|
74
|
+
const INTENSITY_PATTERN = /死|疯了|极了|受不了|爆|炸|太|超|巨|特别|疯狂/
|
|
75
|
+
const REPEATED_PUNCT = /[!!??]{2,}/
|
|
76
|
+
|
|
77
|
+
const HIGH_DOMINANCE = /凭什么|我决定|不许|听我的|我不在乎|随便你|必须|轮不到|说了算|懒得|我才不/g
|
|
78
|
+
const LOW_DOMINANCE = /不敢|没办法|只能|求|拜托|对不起|是我错|控制不住|万一|怎么办|逃|来不及|都怪我/g
|
|
79
|
+
|
|
80
|
+
const CLAUSE_SPLIT = /([,。!?;,.!?;]+)/
|
|
81
|
+
|
|
82
|
+
function clamp(value, min, max) {
|
|
83
|
+
return Math.max(min, Math.min(max, value))
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Normalise a four-axis composition.
|
|
88
|
+
*
|
|
89
|
+
* Returns null for "no mood". An explicit all-zero object normalises to null too: it
|
|
90
|
+
* is the tool-friendly way of saying neutral, not a reading where all four are faintly
|
|
91
|
+
* present. Anything malformed is rejected instead of being repaired — silently
|
|
92
|
+
* clamping a negative share into a legal one would hide a caller bug forever.
|
|
93
|
+
*/
|
|
94
|
+
export function normalizeEmotion(raw) {
|
|
95
|
+
if (raw === null || raw === undefined) return null
|
|
96
|
+
if (typeof raw !== 'object' || Array.isArray(raw)) throw new Error('emotion must be an object with joy, anger, sorrow, fear')
|
|
97
|
+
const values = {}
|
|
98
|
+
for (const axis of EMOTION_AXES) {
|
|
99
|
+
if (!Object.prototype.hasOwnProperty.call(raw, axis)) throw new Error('emotion is missing the ' + axis + ' share')
|
|
100
|
+
const value = raw[axis]
|
|
101
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) throw new Error('emotion.' + axis + ' must be a finite number')
|
|
102
|
+
if (value < 0) throw new Error('emotion.' + axis + ' must not be negative')
|
|
103
|
+
values[axis] = value
|
|
104
|
+
}
|
|
105
|
+
const sum = EMOTION_AXES.reduce((total, axis) => total + values[axis], 0)
|
|
106
|
+
if (!(sum > 0)) return null
|
|
107
|
+
const normalized = {}
|
|
108
|
+
for (const axis of EMOTION_AXES) normalized[axis] = values[axis] / sum
|
|
109
|
+
return normalized
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** The legacy single-number projection. Joy is the only axis that points up. */
|
|
113
|
+
export function derivedValence(emotion) {
|
|
114
|
+
if (!emotion) return 0
|
|
115
|
+
const raw = VALENCE_SCALE * (emotion.joy - emotion.anger - emotion.sorrow - emotion.fear)
|
|
116
|
+
return clamp(Number.isFinite(raw) ? raw : 0, -1, 1)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** The largest single share; 0 for a neutral reading. */
|
|
120
|
+
export function maxShare(emotion) {
|
|
121
|
+
if (!emotion) return 0
|
|
122
|
+
return EMOTION_AXES.reduce((best, axis) => Math.max(best, Number(emotion[axis]) || 0), 0)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** How much of the present composition a memory still shares. 0..1, symmetric. */
|
|
126
|
+
export function overlap(left, right) {
|
|
127
|
+
if (!left || !right) return 0
|
|
128
|
+
let total = 0
|
|
129
|
+
for (const axis of EMOTION_AXES) {
|
|
130
|
+
const a = Number(left[axis]) || 0
|
|
131
|
+
const b = Number(right[axis]) || 0
|
|
132
|
+
if (a > 0 && b > 0) total += Math.min(a, b)
|
|
133
|
+
}
|
|
134
|
+
return clamp(total, 0, 1)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Dominance similarity, mapped to a bounded multiplier. A missing or illegal value on
|
|
139
|
+
* either side is "no signal", never NaN: callers multiply by this result directly.
|
|
140
|
+
*/
|
|
141
|
+
export function dominanceBias(entryDominance, contextDominance) {
|
|
142
|
+
// Number(null) is 0, so a missing value must be rejected before coercion — otherwise
|
|
143
|
+
// "we do not know" quietly becomes "slightly in control" and still moves the ranking.
|
|
144
|
+
if (entryDominance === null || entryDominance === undefined) return 1
|
|
145
|
+
if (contextDominance === null || contextDominance === undefined) return 1
|
|
146
|
+
const a = Number(entryDominance)
|
|
147
|
+
const b = Number(contextDominance)
|
|
148
|
+
if (!Number.isFinite(a) || !Number.isFinite(b)) return 1
|
|
149
|
+
return 1 + 0.2 * (0.5 - Math.abs(clamp(a, -1, 1) - clamp(b, -1, 1)) / 2)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Whether the affect terms are worth applying at all. Three independent legs: a clear
|
|
154
|
+
* composition, an arousal away from rest, or a clear sense of control. A null
|
|
155
|
+
* composition only closes the first leg.
|
|
156
|
+
*/
|
|
157
|
+
export function emotionActive({ emotion = null, arousal = BASE_AROUSAL, dominance = 0 } = {}) {
|
|
158
|
+
if (emotion !== null && emotion !== undefined && maxShare(emotion) >= EMOTION_MIN_SHARE) return true
|
|
159
|
+
const arousalValue = Number(arousal)
|
|
160
|
+
if (Number.isFinite(arousalValue) && Math.abs(arousalValue - BASE_AROUSAL) >= 0.1) return true
|
|
161
|
+
const dominanceValue = Number(dominance)
|
|
162
|
+
if (Number.isFinite(dominanceValue) && Math.abs(dominanceValue) >= 0.25) return true
|
|
163
|
+
return false
|
|
164
|
+
}
|
|
28
165
|
|
|
166
|
+
function clausesOf(source) {
|
|
167
|
+
return String(source || '')
|
|
168
|
+
.split(CLAUSE_SPLIT)
|
|
169
|
+
.map((part) => part.trim())
|
|
170
|
+
.filter((part) => part && !/^[,。!?;,.!?;]+$/.test(part))
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function compoundIn(clause) {
|
|
174
|
+
const hit = COMPOUND_PATTERN.exec(clause)
|
|
175
|
+
return hit ? COMPOUND_PRESETS[hit[0]] : null
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function axisHits(clause) {
|
|
179
|
+
const hits = {}
|
|
180
|
+
for (const axis of EMOTION_AXES) {
|
|
181
|
+
let best = 0
|
|
182
|
+
for (const [pattern, weight] of AXIS_PATTERNS[axis]) {
|
|
183
|
+
if (pattern.test(clause)) best = Math.max(best, weight)
|
|
184
|
+
}
|
|
185
|
+
if (best > 0) hits[axis] = best
|
|
186
|
+
}
|
|
187
|
+
return hits
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Read the present affect state out of one user turn.
|
|
192
|
+
*
|
|
193
|
+
* Kept text-only and deterministic. `.valence` stays on the return value because the
|
|
194
|
+
* legacy retrieval path and its tests read it directly; `emotion`/`dominance` are
|
|
195
|
+
* additive, so a caller that only knows about valence keeps working.
|
|
196
|
+
*/
|
|
29
197
|
export function readMood(text) {
|
|
30
198
|
const source = String(text || '')
|
|
31
199
|
.replace(/["“「][^"”」]*["”」]/g, '')
|
|
32
|
-
.replace(
|
|
33
|
-
.replace(
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
let
|
|
39
|
-
|
|
40
|
-
|
|
200
|
+
.replace(NEGATION_AS_SAD, ' sad ')
|
|
201
|
+
.replace(NEGATION_TO_NEUTRAL, '')
|
|
202
|
+
const clauses = clausesOf(source)
|
|
203
|
+
|
|
204
|
+
const raw = { joy: 0, anger: 0, sorrow: 0, fear: 0 }
|
|
205
|
+
let compoundDominance = null
|
|
206
|
+
let peakIntensity = 0
|
|
207
|
+
let hedged = false
|
|
208
|
+
|
|
209
|
+
for (const clause of clauses) {
|
|
210
|
+
const hedge = HEDGE_PATTERN.test(clause) && !HEDGE_SKEW_GUARD.test(clause)
|
|
211
|
+
if (hedge) hedged = true
|
|
212
|
+
const scale = hedge ? 0.6 : 1
|
|
213
|
+
const compound = compoundIn(clause)
|
|
214
|
+
if (compound) {
|
|
215
|
+
for (const axis of EMOTION_AXES) raw[axis] += compound.emotion[axis] * scale
|
|
216
|
+
if (compoundDominance === null) compoundDominance = compound.dominance
|
|
217
|
+
} else {
|
|
218
|
+
const hits = axisHits(clause)
|
|
219
|
+
for (const axis of EMOTION_AXES) if (hits[axis]) raw[axis] += hits[axis] * scale
|
|
220
|
+
}
|
|
221
|
+
if (INTENSITY_PATTERN.test(clause) || REPEATED_PUNCT.test(clause)) peakIntensity = Math.max(peakIntensity, 0.75)
|
|
222
|
+
if (REPEATED_PUNCT.test(clause)) peakIntensity = Math.max(peakIntensity, 0.8)
|
|
41
223
|
}
|
|
224
|
+
|
|
225
|
+
for (const axis of EMOTION_AXES) raw[axis] = Math.min(1, raw[axis])
|
|
226
|
+
const emotion = normalizeEmotion(raw)
|
|
227
|
+
|
|
228
|
+
let arousal = BASE_AROUSAL
|
|
229
|
+
if (emotion !== null) arousal = Math.max(arousal, peakIntensity)
|
|
42
230
|
if (source.length > 600) arousal = Math.max(0.2, arousal - 0.15)
|
|
231
|
+
|
|
232
|
+
const high = (source.match(HIGH_DOMINANCE) || []).length
|
|
233
|
+
const low = (source.match(LOW_DOMINANCE) || []).length
|
|
234
|
+
const lexical = clamp(0.5 * (high - low), -1, 1)
|
|
235
|
+
const prior = emotion === null ? 0 : DOMINANCE_PRIOR_WEIGHT * (emotion.anger - emotion.fear)
|
|
236
|
+
const dominance = clamp(lexical + prior + (compoundDominance === null ? 0 : compoundDominance * 0.5), -1, 1)
|
|
237
|
+
|
|
43
238
|
return {
|
|
44
|
-
valence:
|
|
45
|
-
|
|
239
|
+
valence: derivedValence(emotion),
|
|
240
|
+
emotion,
|
|
241
|
+
arousal: clamp(arousal, 0, 1),
|
|
242
|
+
dominance: hedged ? clamp(dominance * 0.8, -1, 1) : dominance,
|
|
46
243
|
}
|
|
47
244
|
}
|
|
48
245
|
|
|
246
|
+
/**
|
|
247
|
+
* Fade a reading toward neutral.
|
|
248
|
+
*
|
|
249
|
+
* The composition does not decay proportionally — proportions sum to 1, so scaling
|
|
250
|
+
* them changes nothing. What fades is how much of it is left: arousal returns to rest,
|
|
251
|
+
* dominance returns to zero, and once the whole reading has faded past the floor the
|
|
252
|
+
* composition is dropped. Arousal and dominance keep decaying past that point.
|
|
253
|
+
*/
|
|
49
254
|
export function moodDecay(mood, now = Date.now()) {
|
|
50
|
-
|
|
51
|
-
const minutes = Math.max(0, (now - mood.at) / 60_000)
|
|
52
|
-
const fade = Math.pow(0.5, minutes / MOOD_HALF_LIFE_MINUTES)
|
|
53
|
-
|
|
255
|
+
const hasStamp = Boolean(mood) && typeof mood.at === 'number'
|
|
256
|
+
const minutes = hasStamp ? Math.max(0, (now - mood.at) / 60_000) : 0
|
|
257
|
+
const fade = hasStamp ? Math.pow(0.5, minutes / MOOD_HALF_LIFE_MINUTES) : 0
|
|
258
|
+
|
|
259
|
+
const emotion = hasStamp && mood.emotion ? mood.emotion : null
|
|
260
|
+
const keptEmotion = fade < EMOTION_FADE_FLOOR ? null : emotion
|
|
261
|
+
const arousalValue = Number(mood && mood.arousal)
|
|
262
|
+
const dominanceValue = Number(mood && mood.dominance)
|
|
263
|
+
const legacyValence = Number(mood && mood.valence)
|
|
264
|
+
|
|
54
265
|
return {
|
|
55
|
-
valence: Number(
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
266
|
+
valence: (keptEmotion ? derivedValence(keptEmotion) : (Number.isFinite(legacyValence) ? legacyValence : 0)) * (hasStamp ? fade : 0),
|
|
267
|
+
emotion: keptEmotion,
|
|
268
|
+
arousal: BASE_AROUSAL + ((Number.isFinite(arousalValue) ? arousalValue : BASE_AROUSAL) - BASE_AROUSAL) * fade,
|
|
269
|
+
dominance: (Number.isFinite(dominanceValue) ? dominanceValue : 0) * fade,
|
|
59
270
|
}
|
|
60
271
|
}
|
|
61
272
|
|
|
62
273
|
// Congruence is |a·b| over signed valence: sad recalls sad, happy recalls happy,
|
|
63
|
-
// and a neutral present state recalls nothing extra.
|
|
274
|
+
// and a neutral present state recalls nothing extra. Kept for the legacy path.
|
|
64
275
|
export function moodCongruence(entryValence, currentValence) {
|
|
65
276
|
const a = Number(entryValence || 0)
|
|
66
277
|
const b = Number(currentValence || 0)
|
|
@@ -69,3 +280,21 @@ export function moodCongruence(entryValence, currentValence) {
|
|
|
69
280
|
const agreement = Math.min(Math.abs(a), Math.abs(b))
|
|
70
281
|
return same ? agreement : -agreement * 0.5
|
|
71
282
|
}
|
|
283
|
+
|
|
284
|
+
export default {
|
|
285
|
+
EMOTION_AXES,
|
|
286
|
+
MOOD_HALF_LIFE_MINUTES,
|
|
287
|
+
BASE_AROUSAL,
|
|
288
|
+
EMOTION_MIN_SHARE,
|
|
289
|
+
EMOTION_FADE_FLOOR,
|
|
290
|
+
COMPOUND_PRESETS,
|
|
291
|
+
normalizeEmotion,
|
|
292
|
+
derivedValence,
|
|
293
|
+
maxShare,
|
|
294
|
+
overlap,
|
|
295
|
+
dominanceBias,
|
|
296
|
+
emotionActive,
|
|
297
|
+
readMood,
|
|
298
|
+
moodDecay,
|
|
299
|
+
moodCongruence,
|
|
300
|
+
}
|
package/lib/preset-scope.js
CHANGED
|
@@ -76,7 +76,7 @@ export function attachPresetAgents(ctx, { log = () => {}, attached = attachments
|
|
|
76
76
|
seen.add(actx)
|
|
77
77
|
if (!attached.has(actx) && !sameScopeTree(actx, ctx)) {
|
|
78
78
|
try {
|
|
79
|
-
const dispose = registerInjection(actx
|
|
79
|
+
const dispose = registerInjection(actx)
|
|
80
80
|
attached.set(actx, dispose)
|
|
81
81
|
added += 1
|
|
82
82
|
log(`attached memory injection to preset conversation ${sessionId}`)
|