@rain13sad/dsh-mnemo-affect 0.0.0-stage → 0.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/README.md CHANGED
@@ -1,3 +1,55 @@
1
- # Temporary Holding Version
1
+ # @rain13sad/dsh-mnemo-affect
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Affect kernel — appraisal, mood, persona and the user schema.
4
+
5
+ Part of **MnemoVault** — long-term memory and affect for
6
+ [DeepSeek Harness](https://github.com/Rain13Sad/bcf-mnemovault). Sparse-code memory lattice, four-channel recall,
7
+ biphasic consolidation, ~116 tokens injected per turn, zero runtime dependencies.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm i @rain13sad/dsh-mnemo-affect
13
+ ```
14
+
15
+ Loaded by the harness as a Cordis plugin — declare it in your plugin list and it applies itself.
16
+
17
+ ## What it does
18
+
19
+ Scores each turn on valence/arousal/dominance, integrates a slow mood, keeps a
20
+ relational state and a persona profile, and turns all of it into a retrieval policy
21
+ that biases what gets recalled.
22
+
23
+ ## Exports
24
+
25
+ - `AffectService`
26
+ - `appraise`
27
+ - `integrateMood`
28
+ - `derivePolicy`
29
+ - `deriveUserSchema`
30
+ - `clampPersona`
31
+
32
+ ## The family
33
+
34
+ | Package | Role |
35
+ | :--- | :--- |
36
+ | `@rain13sad/dsh-mnemo-core` | Lattice, store, index and renderer — the `ctx.memory` service. |
37
+ | `@rain13sad/dsh-mnemo-affect` | Affect kernel — appraisal, mood, persona and the user schema. |
38
+ | `@rain13sad/dsh-mnemo-encoder` | Write path — turns a finished turn into sparse-code cells. |
39
+ | `@rain13sad/dsh-mnemo-recall` | Read path — four-channel recall and prompt injection. |
40
+ | `@rain13sad/dsh-mnemo-consolidator` | Offline consolidation — NREM, REM and forgetting. |
41
+ | `@rain13sad/dsh-mnemo-proactive` | Companion layer — when the agent may speak first. |
42
+ | `@rain13sad/dsh-mnemo-tools` | Tool surface and `/mnemo` commands. |
43
+ | `@rain13sad/dsh-mnemo-settings` | Settings schema, presets and the effect matrix. |
44
+ | `@rain13sad/dsh-mnemo-devkit` | Host-free runner — mock Cordis context and session replay. |
45
+ | `@rain13sad/dsh-mnemo-ui` | Sidebar panel — memory browser, graph, affect and diagnostics. |
46
+
47
+ ## Links
48
+
49
+ - Repository and full docs: https://github.com/Rain13Sad/bcf-mnemovault
50
+ - 中文说明: https://github.com/Rain13Sad/bcf-mnemovault/blob/main/README.zh-CN.md
51
+ - Design notes: https://github.com/Rain13Sad/bcf-mnemovault/blob/main/docs/design/MNEMOVAULT-DESIGN.md
52
+
53
+ ## License
54
+
55
+ MIT
@@ -0,0 +1,9 @@
1
+ # 情感内核:内感受 → 评价 → PAD → 心境 → 关系模型
2
+ - insert:
3
+ - id: mnemo-affect
4
+ name: '@rain13sad/dsh-mnemo-affect'
5
+ config:
6
+ moodTau: 20
7
+ personaDriftEpsilon: 0.25
8
+ negativeWeight: 3
9
+ enabled: true
@@ -0,0 +1,111 @@
1
+ /**
2
+ * 评价 → 核心情感(对应 §11.2 / §11.3)
3
+ *
4
+ * 立场(不可让渡):这满足"有需要维持的内部状态",**不满足**"有主观体验"。
5
+ * 所有输出都是状态变量的投影;标签("焦虑"/"满意")只在渲染时生成,不入库。
6
+ */
7
+ const clamp = (x, lo = -1, hi = 1) => Math.min(hi, Math.max(lo, x));
8
+ /**
9
+ * 六维评价。规则可解释、可消融、可回归——这是"情感是参数不是台词"的前提(A3)。
10
+ * 它当然不如 LLM 标注细腻,但它的每一次输出都能被指着说"因为这句话里的这个词"。
11
+ */
12
+ export function appraise(input) {
13
+ const t = input.text;
14
+ // 陪伴场景的词汇表和"客服场景"完全不同:用户说的是"难受""累""委屈",
15
+ // 不是"不满意""差评"。这张表是这套系统能不能"听懂情绪"的第一道关。
16
+ const pos = countMatches(t,
17
+ // 不保留裸的"好/对/差":'不好' 里的 '好' 会把否定句算成正向(实测踩过)
18
+ /很好|真好|太好了|好棒|不错|开心|高兴|舒服|轻松|喜欢|爱|暖|安心|踏实|感动|幸福|爽|辛苦了|谢谢|满意|赞|great|thanks|perfect|nice|good|happy|love/gi);
19
+ const neg = countMatches(t, /不好|不对|错了|糟糕|垃圾|烦|失望|不行|太慢|难受|难过|累|疲惫|委屈|生气|愤怒|焦虑|害怕|孤独|寂寞|崩溃|撑不住|想哭|哭|郁闷|压抑|无助|绝望|心累|头疼|压力|烦死|受不了|算了|没用|做不到|broken|wrong|bad|hate|annoy|tired|sad|angry|lonely|anxious/gi);
20
+ const hedge = countMatches(t, /可能|也许|大概|不确定|好像|maybe|perhaps|not sure|I think/gi);
21
+ const novel = input.topic && input.seenTopics && !input.seenTopics.has(input.topic) ? 1 : 0;
22
+ const goalHit = (input.goalEntities ?? []).filter((e) => e && t.toLowerCase().includes(e.toLowerCase())).length;
23
+ const goalRel = input.goalEntities?.length ? Math.min(1, 0.4 + goalHit / Math.max(1, input.goalEntities.length)) : 0.35;
24
+ const goalCon = clamp((pos - neg) / 3 + (input.lastAssistantRejected ? -0.4 : 0));
25
+ const agency = clamp(0.5 + pos * 0.1 - neg * 0.15);
26
+ const certainty = clamp(0.6 - hedge * 0.15);
27
+ const novelty = clamp(novel * 0.7 + Math.min(1, t.length / 400) * 0.3);
28
+ const norm = countMatches(t, /请|麻烦|谢谢|please|kindly/gi) > 0 ? 0.4 : 0;
29
+ // 情绪强度:正负抵消后剩下的"带电程度",按出现次数饱和。
30
+ // '今天真的很难受' → pos 0 / neg 1 → charge = -0.33,足以把效价推到 -0.5 以下。
31
+ const total = pos + neg;
32
+ const charge = total === 0 ? 0 : ((pos - neg) / total) * Math.min(1, total / 3);
33
+ return { goalRel, goalCon, agency, certainty, novelty, norm, charge };
34
+ }
35
+ /**
36
+ * PAD + 显著度(§11.3 的四个式子,一字未改)。
37
+ * 设计要点:tanh 保证有界且两端饱和;arousal 显式依赖 load(负荷高时同样评价产生更高唤起);
38
+ * dominance 依赖 uncert(负向)。所有系数可配置、可消融。
39
+ */
40
+ export function coreAffect(a, i, coeff = DEFAULT_COEFF) {
41
+ const c = coeff;
42
+ const v = Math.tanh(c.v1 * a.goalRel * a.goalCon + c.v2 * a.novelty * a.goalCon + c.v3 * a.norm + c.v4 * (a.charge ?? 0));
43
+ const ar = Math.tanh(c.a1 * a.novelty + c.a2 * Math.abs(a.goalCon) * a.goalRel + c.a3 * i.load);
44
+ const d = Math.tanh(c.d1 * a.agency + c.d2 * a.certainty - c.d3 * i.uncert);
45
+ const s = Math.tanh(c.s1 * a.goalRel + c.s2 * Math.abs(a.goalCon) + c.s3 * a.novelty);
46
+ return { v, a: ar, d, s: (s + 1) / 2 };
47
+ }
48
+ export const DEFAULT_COEFF = {
49
+ v1: 1.2,
50
+ v4: 1.1,
51
+ v2: 0.4,
52
+ v3: 0.3,
53
+ a1: 0.8,
54
+ a2: 0.6,
55
+ a3: 0.5,
56
+ d1: 0.7,
57
+ d2: 0.3,
58
+ d3: 0.2,
59
+ s1: 0.9,
60
+ s2: 0.7,
61
+ s3: 0.4,
62
+ };
63
+ /** 心境积分(§11.4):m ← m + (e − m)/τ。τ≈20 轮。这层是稳定性来源,没有它 persona 会碎。 */
64
+ export function integrateMood(mood, e, tau) {
65
+ const k = 1 / Math.max(1, tau);
66
+ return {
67
+ v: mood.v + (e.v - mood.v) * k,
68
+ a: mood.a + (e.a - mood.a) * k,
69
+ d: mood.d + (e.d - mood.d) * k,
70
+ s: mood.s + (e.s - mood.s) * k,
71
+ };
72
+ }
73
+ export function neutralPad() {
74
+ return { v: 0, a: 0.2, d: 0.5, s: 0.3 };
75
+ }
76
+ export function neutralIntero() {
77
+ return { load: 0.2, uncert: 0.2, conflict: 0, strain: 0, progress: 0.5, budget: 0.9 };
78
+ }
79
+ /**
80
+ * 情绪标签:只在渲染时生成,不入库(§11.2 ④)。
81
+ * 注意措辞:这是"状态描述",不是"我感到"。§19.3 禁止输出"我感到…"。
82
+ */
83
+ export function label(p) {
84
+ if (p.v > 0.35 && p.a < 0.4)
85
+ return '平稳积极';
86
+ if (p.v > 0.2 && p.a >= 0.4)
87
+ return '投入';
88
+ if (p.v < -0.3 && p.a > 0.55)
89
+ return '紧张';
90
+ if (p.v < -0.2 && p.a <= 0.55)
91
+ return '低落';
92
+ if (p.a > 0.7)
93
+ return '高唤起';
94
+ if (Math.abs(p.v) < 0.15)
95
+ return '中性';
96
+ return p.v > 0 ? '略积极' : '略消极';
97
+ }
98
+ /** 人格漂移钳制(§11.8 修 D-13):只在超界时收缩,绝不放大。 */
99
+ export function clampPersona(persona, persona0, eps) {
100
+ const d = persona.map((x, i) => x - (persona0[i] ?? 0));
101
+ const n = Math.sqrt(d.reduce((a, b) => a + b * b, 0));
102
+ if (n <= eps)
103
+ return persona.slice(); // 关键:不放大
104
+ const k = eps / n;
105
+ return persona0.map((x, i) => x + d[i] * k);
106
+ }
107
+ function countMatches(text, re) {
108
+ const m = text.match(re);
109
+ return m ? m.length : 0;
110
+ }
111
+ //# sourceMappingURL=appraisal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"appraisal.js","sourceRoot":"","sources":["../src/appraisal.ts"],"names":[],"mappings":"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"}
package/lib/index.js ADDED
@@ -0,0 +1,466 @@
1
+ /**
2
+ * @rain13sad/dsh-mnemo-affect —— 情感内核(§11 / §14.2 / §15)
3
+ *
4
+ * 四段管线:interocept → core affect → appraisal×context → label(仅渲染时)
5
+ * 对应 Barrett 四步:身体变化 → 感知 → 结合经验解释 → 情绪出现。
6
+ *
7
+ * 边界(代码注释与对外表述都要守住):
8
+ * · 这满足"有需要维持的内部状态",**不满足**"有主观体验"
9
+ * · `report()` 返回数值,不返回感受
10
+ * · 任何"我感到…"的措辞都不在此产生
11
+ */
12
+ import { Service } from '@deepseek-ai/cordis';
13
+ import z from '@deepseek-ai/schemastery';
14
+ import { neutralIntero } from './appraisal.js';
15
+ import { appraise, clampPersona, coreAffect, DEFAULT_COEFF, integrateMood, label, neutralPad } from './appraisal.js';
16
+ import { derivePolicy, interocept } from './policy.js';
17
+ import { initialRelational, personaFromAttunement, updateRelational } from './relational.js';
18
+ import { deriveUserSchema, personaCell, readPersisted, relationalCell, schemaCell, } from './persist.js';
19
+ export const name = 'mnemo-affect';
20
+ export const inject = ['sessions'];
21
+ export const Config = z.object({
22
+ moodTau: z.number().min(5).max(60).default(20).description('心境惯性:τ 越大越稳,越小越跳变'),
23
+ personaDriftEpsilon: z.number().min(0).max(0.5).default(0.25).description('人格漂移上限。只在超界时收缩,绝不放大(修 D-13)'),
24
+ negativeWeight: z.number().min(1).max(6).default(3).description('关系负面权重。调高更保守,是反谄媚手段'),
25
+ enabled: z.boolean().default(true).description('关闭后走降级路径:不做情感门控,也不注入情感状态行'),
26
+ coeff: z.object({
27
+ v1: z.number().default(DEFAULT_COEFF.v1),
28
+ v4: z.number().default(DEFAULT_COEFF.v4),
29
+ v2: z.number().default(DEFAULT_COEFF.v2),
30
+ v3: z.number().default(DEFAULT_COEFF.v3),
31
+ a1: z.number().default(DEFAULT_COEFF.a1),
32
+ a2: z.number().default(DEFAULT_COEFF.a2),
33
+ a3: z.number().default(DEFAULT_COEFF.a3),
34
+ d1: z.number().default(DEFAULT_COEFF.d1),
35
+ d2: z.number().default(DEFAULT_COEFF.d2),
36
+ d3: z.number().default(DEFAULT_COEFF.d3),
37
+ s1: z.number().default(DEFAULT_COEFF.s1),
38
+ s2: z.number().default(DEFAULT_COEFF.s2),
39
+ s3: z.number().default(DEFAULT_COEFF.s3),
40
+ }),
41
+ });
42
+ export class AffectService extends Service {
43
+ config;
44
+ intero = neutralIntero();
45
+ pad = neutralPad();
46
+ mood = neutralPad();
47
+ relational = initialRelational();
48
+ persona = [0.5, 0.5, 0.5, 0.5];
49
+ persona0 = [0.5, 0.5, 0.5, 0.5];
50
+ lastTurn = null;
51
+ /** 人格版本链(§11.8 可回滚) */
52
+ personaRev = 0;
53
+ personaHistory = [];
54
+ /** 离线环的原始样本:每轮的 PAD / 张力 / 是否被纠正 */
55
+ history = [];
56
+ lastPersistTurn = -99;
57
+ lastSchemaTurn = 0;
58
+ lastSchema = [];
59
+ seenTopics = new Set();
60
+ /** 情绪事件(一次显著的起伏),带衰减;供面板画轨迹、供主动聊天挑话题 */
61
+ episodes = [];
62
+ /** PAD 轨迹(每轮一个点,上限 240,够画 40 分钟的连续情绪) */
63
+ trajectory = [];
64
+ /** 有情绪波动的轮数,用于依恋度 */
65
+ emotionalTurns = 0;
66
+ negativeAccum = 0;
67
+ repeats = 0;
68
+ constructor(ctx, config) {
69
+ super(ctx, 'affect');
70
+ this.config = config;
71
+ }
72
+ /** 当前情绪(本轮),供 recall 做门控与打分。 */
73
+ current() {
74
+ return this.pad;
75
+ }
76
+ currentMood() {
77
+ return this.mood;
78
+ }
79
+ currentIntero() {
80
+ return this.intero;
81
+ }
82
+ relationalSnapshot() {
83
+ const { trust, familiarity, attunement, boundaries, repairDebt, turns } = this.relational;
84
+ return { trust, familiarity, attunement, boundaries: [...boundaries], repairDebt, turns };
85
+ }
86
+ /** 六变量可手动注入(调试用,`/mnemo affect set`)。 */
87
+ setIntero(patch) {
88
+ this.intero = { ...this.intero, ...patch };
89
+ return this.intero;
90
+ }
91
+ /** 一轮的完整评价(快环)。 */
92
+ observe(input) {
93
+ if (!this.config.enabled) {
94
+ return {
95
+ appraisal: { goalRel: 0, goalCon: 0, agency: 0, certainty: 0, novelty: 0, norm: 0 },
96
+ pad: neutralPad(),
97
+ mood: this.mood,
98
+ relational: this.relationalSnapshot(),
99
+ policy: derivePolicy({ pad: this.pad, mood: this.mood, intero: this.intero, relational: this.relationalSnapshot(), turns: this.relational.turns }),
100
+ intero: this.intero,
101
+ label: '已关闭',
102
+ };
103
+ }
104
+ const a = appraise({
105
+ text: input.text,
106
+ goalEntities: input.goalEntities,
107
+ seenTopics: this.seenTopics,
108
+ topic: input.topic,
109
+ lastAssistantRejected: input.lastAssistantRejected,
110
+ });
111
+ this.intero = interocept({
112
+ ...input.signals,
113
+ negativeAccum: this.negativeAccum,
114
+ repeatRate: this.repeats,
115
+ }, this.intero);
116
+ const e = coreAffect(a, this.intero, this.config.coeff);
117
+ this.pad = e;
118
+ this.mood = integrateMood(this.mood, e, this.config.moodTau);
119
+ this.negativeAccum = Math.max(0, this.negativeAccum * 0.85 + (e.v < -0.2 ? 0.15 : 0));
120
+ if (input.repeated)
121
+ this.repeats = Math.min(1, this.repeats + 0.2);
122
+ else
123
+ this.repeats = Math.max(0, this.repeats * 0.9);
124
+ const rel = updateRelational(this.relational, {
125
+ text: input.text,
126
+ turn: input.turn,
127
+ repeated: input.repeated,
128
+ promise: input.promise,
129
+ negativeWeight: this.config.negativeWeight,
130
+ });
131
+ this.relational = rel.model;
132
+ // persona 微调 + 有界漂移(§11.8)
133
+ const tune = personaFromAttunement(this.relational.attunement);
134
+ this.persona = clampPersona([0.5 + tune.hedging * 0.5, 0.5, 0.5 + (tune.clarification ? 0.1 : 0), 0.5 - tune.hedging * 0.3], this.persona0, this.config.personaDriftEpsilon);
135
+ if (input.topic)
136
+ this.seenTopics.add(input.topic);
137
+ const policy = derivePolicy({
138
+ pad: this.pad,
139
+ mood: this.mood,
140
+ intero: this.intero,
141
+ relational: this.relationalSnapshot(),
142
+ turns: this.relational.turns,
143
+ hasGoal: (input.goalEntities?.length ?? 0) > 0,
144
+ });
145
+ this.lastTurn = {
146
+ appraisal: a,
147
+ pad: this.pad,
148
+ mood: this.mood,
149
+ relational: this.relationalSnapshot(),
150
+ policy,
151
+ intero: this.intero,
152
+ label: label(this.pad),
153
+ };
154
+ // 情绪轨迹与事件:这是"情感是动态的"最直接的证据——
155
+ // 面板上那条折线就是它,主动聊天判断"该不该说话"也看它。
156
+ const prev = this.trajectory[this.trajectory.length - 1];
157
+ this.trajectory.push({ turn: input.turn, v: this.pad.v, a: this.pad.a, d: this.pad.d });
158
+ if (this.trajectory.length > 240)
159
+ this.trajectory.shift();
160
+ if (Math.abs(this.pad.v) >= 0.45)
161
+ this.emotionalTurns++;
162
+ const swing = prev ? Math.abs(this.pad.v - prev.v) : 0;
163
+ if (swing > 0.3 || this.pad.a > 0.7) {
164
+ this.episodes.push({
165
+ at: Date.now(),
166
+ turn: input.turn,
167
+ peak: { v: this.pad.v, a: this.pad.a },
168
+ trigger: input.text.slice(0, 40),
169
+ halfLife: 6,
170
+ });
171
+ if (this.episodes.length > 40)
172
+ this.episodes.shift();
173
+ }
174
+ // 离线环的样本:只记"这一轮发生了什么",不做解释
175
+ if (input.topic) {
176
+ this.history.push({
177
+ turn: input.turn,
178
+ topic: input.topic,
179
+ pad: this.pad,
180
+ strain: this.intero.strain,
181
+ corrected: /不对|错了|不是这个|我说的是/.test(input.text),
182
+ });
183
+ if (this.history.length > 500)
184
+ this.history.shift();
185
+ }
186
+ // 慢环落盘:每 5 轮写一次关系模型与人格锚(每轮都写会把库刷成日志)
187
+ if (input.turn - this.lastPersistTurn >= 5) {
188
+ this.lastPersistTurn = input.turn;
189
+ this.persist();
190
+ }
191
+ // 离线环:每 40 轮抽一次用户图式
192
+ if (input.turn - this.lastSchemaTurn >= 40 && this.history.length >= 12) {
193
+ this.lastSchemaTurn = input.turn;
194
+ this.refreshSchema();
195
+ }
196
+ return this.lastTurn;
197
+ }
198
+ /** 人格快照(面板与命令用)。 */
199
+ personaState() {
200
+ return { rev: this.personaRev, vector: [...this.persona], baseline: [...this.persona0], history: this.personaHistory.slice(-10) };
201
+ }
202
+ /** 人格回滚(§11.8):回滚也算一次新版本,不做原地改写。 */
203
+ rollbackPersona(rev) {
204
+ const target = rev === 0 ? { rev: 0, vector: [...this.persona0] } : this.personaHistory.find((h) => h.rev === rev);
205
+ if (!target)
206
+ return { ok: false, message: `找不到 rev ${rev}` };
207
+ this.persona = [...target.vector];
208
+ this.personaRev += 1;
209
+ this.personaHistory.push({ rev: this.personaRev, vector: [...this.persona] });
210
+ this.persist();
211
+ return { ok: true, message: `已回滚到 rev ${rev}(新版本 rev ${this.personaRev})` };
212
+ }
213
+ /**
214
+ * 依恋度(0..1):熟悉度 40% + 信任 30% + 共同情绪事件 30%。
215
+ * 刻意不包含"轮数"——刷轮数涨上来的不是依恋,是时长。
216
+ */
217
+ attachment() {
218
+ const rel = this.relational;
219
+ const shared = Math.min(1, this.emotionalTurns / 20);
220
+ const a = 0.4 * rel.familiarity + 0.3 * Math.max(0, rel.trust) + 0.3 * shared;
221
+ return Math.round(a * 1000) / 1000;
222
+ }
223
+ /** 关系阶段:陌生 → 熟悉 → 亲密 → 依赖(阈值与面板文案绑定,改要一起改) */
224
+ stage() {
225
+ const a = this.attachment();
226
+ if (a < 0.2)
227
+ return { key: 'stranger', label: '陌生', progress: a / 0.2 };
228
+ if (a < 0.45)
229
+ return { key: 'familiar', label: '熟悉', progress: (a - 0.2) / 0.25 };
230
+ if (a < 0.7)
231
+ return { key: 'close', label: '亲密', progress: (a - 0.45) / 0.25 };
232
+ return { key: 'attached', label: '依赖', progress: Math.min(1, (a - 0.7) / 0.3) };
233
+ }
234
+ /** 情绪轨迹(面板画折线用) */
235
+ trajectorySeries() {
236
+ return this.trajectory.slice(-120);
237
+ }
238
+ /** 未平复的情绪事件(峰值 × 半衰),供主动聊天判断"他还在气头上吗" */
239
+ activeEpisodes(now = Date.now()) {
240
+ return this.episodes
241
+ .map((e) => ({
242
+ turn: e.turn,
243
+ v: e.peak.v,
244
+ a: e.peak.a,
245
+ strength: Math.min(1, Math.abs(e.peak.v) * Math.pow(0.5, (now - e.at) / (e.halfLife * 3600_000))),
246
+ trigger: e.trigger,
247
+ }))
248
+ .filter((e) => e.strength > 0.15)
249
+ .sort((x, y) => y.strength - x.strength);
250
+ }
251
+ /**
252
+ * 情感再评估(reappraisal):用**当前心境**重估一条记忆的效价。
253
+ * 记忆是建构性的——同一件事,心情好的时候想起来没那么糟。
254
+ * 只调 valence(情感维度),不动内容、不动 support(证据数不是情绪)。
255
+ */
256
+ reappraise(cellId) {
257
+ const memory = this.ctx.get('memory');
258
+ if (!memory)
259
+ return { ok: false, before: 0, after: 0, message: '记忆服务未就绪' };
260
+ const id = BigInt('0x' + cellId.replace(/^0x/, '').padStart(16, '0'));
261
+ const cell = memory.store.getCell(id);
262
+ if (!cell)
263
+ return { ok: false, before: 0, after: 0, message: 'cell 不存在' };
264
+ const before = cell.valence;
265
+ // 0.6 保留原值 + 0.4 受当前心境影响:一次重估不会翻脸
266
+ cell.valence = Math.max(-1, Math.min(1, before * 0.6 + this.mood.v * 0.4));
267
+ cell.salience = Math.min(1, cell.salience * 0.95 + 0.05);
268
+ memory.store.putCell(cell);
269
+ memory.publishPanel();
270
+ return {
271
+ ok: true,
272
+ before,
273
+ after: cell.valence,
274
+ message: `效价 ${before.toFixed(2)} → ${cell.valence.toFixed(2)}(心境 ${this.mood.v.toFixed(2)})`,
275
+ };
276
+ }
277
+ /** 当前用户图式(离线环产物)。 */
278
+ schema() {
279
+ return this.lastSchema.slice();
280
+ }
281
+ /** 慢环落盘:关系模型 + 人格锚。失败不影响情感计算。 */
282
+ persist() {
283
+ const memory = this.ctx.get('memory');
284
+ if (!memory)
285
+ return;
286
+ const now = Date.now();
287
+ try {
288
+ this.personaRev += 1;
289
+ this.personaHistory.push({ rev: this.personaRev, vector: [...this.persona] });
290
+ if (this.personaHistory.length > 50)
291
+ this.personaHistory.shift();
292
+ memory.ingest(relationalCell(memory, this.relationalSnapshot(), { rev: this.personaRev, vector: this.persona }, now));
293
+ memory.ingest(personaCell(memory, { rev: this.personaRev, vector: this.persona }, now));
294
+ // 版本链不是日志:只留最近 12 版,更早的软删(可回溯,不物理删)
295
+ const olds = memory.store.listCells({ kind: 5, limit: 50, order: 'recent' }).slice(12);
296
+ for (const c of olds)
297
+ memory.forget(c.cellId.toString(16).padStart(16, '0'), 'soft', 'policy', 'persona 版本链截断');
298
+ }
299
+ catch {
300
+ // 落盘失败不能影响本轮情感输出:这是"降级"而不是"故障"
301
+ }
302
+ }
303
+ /** 离线环:抽图式并写 kind=3 cell。 */
304
+ refreshSchema() {
305
+ const patterns = deriveUserSchema(this.history);
306
+ this.lastSchema = patterns;
307
+ const memory = this.ctx.get('memory');
308
+ if (!memory)
309
+ return patterns;
310
+ for (const p of patterns) {
311
+ try {
312
+ memory.ingest(schemaCell(memory, p, Date.now()));
313
+ }
314
+ catch {
315
+ /* 单条失败不影响其它 */
316
+ }
317
+ }
318
+ return patterns;
319
+ }
320
+ /** 热重载 / 重启后从库恢复(§14.7 可重建内存缓存)。 */
321
+ restoreFromMemory() {
322
+ const memory = this.ctx.get('memory');
323
+ if (!memory)
324
+ return false;
325
+ const saved = readPersisted(memory);
326
+ if (saved.relational)
327
+ this.relational = { ...this.relational, ...saved.relational, lastAttuneTurn: -1 };
328
+ if (saved.persona) {
329
+ this.persona = saved.persona.vector.slice(0, 4);
330
+ this.personaRev = saved.persona.rev;
331
+ }
332
+ return Boolean(saved.relational || saved.persona);
333
+ }
334
+ policy() {
335
+ return derivePolicy({
336
+ pad: this.pad,
337
+ mood: this.mood,
338
+ intero: this.intero,
339
+ relational: this.relationalSnapshot(),
340
+ turns: this.relational.turns,
341
+ });
342
+ }
343
+ /** affect_report 的数据源:**只有数值**,没有感受。 */
344
+ report() {
345
+ const p = this.pad;
346
+ return {
347
+ intero_load: round(this.intero.load),
348
+ intero_uncert: round(this.intero.uncert),
349
+ intero_conflict: round(this.intero.conflict),
350
+ intero_strain: round(this.intero.strain),
351
+ intero_progress: round(this.intero.progress),
352
+ intero_budget: round(this.intero.budget),
353
+ pad_valence: round(p.v),
354
+ pad_arousal: round(p.a),
355
+ pad_dominance: round(p.d),
356
+ pad_salience: round(p.s),
357
+ mood_valence: round(this.mood.v),
358
+ trust: round(this.relational.trust),
359
+ familiarity: round(this.relational.familiarity),
360
+ attunement: round(this.relational.attunement),
361
+ repair_debt: this.relational.repairDebt,
362
+ boundaries: this.relational.boundaries.join(';'),
363
+ label: label(p),
364
+ disclaimer: '这些是内部状态数值,不是主观体验,也不代表拥有情绪。',
365
+ };
366
+ }
367
+ /** 面板需要、但不属于"本轮评价结果"的部分。 */
368
+ snapshotExtras() {
369
+ return {
370
+ persona: { rev: this.personaRev, vector: [...this.persona], baseline: [...this.persona0] },
371
+ schema: this.lastSchema.slice(),
372
+ attachment: this.attachment(),
373
+ stage: this.stage(),
374
+ trajectory: this.trajectorySeries(),
375
+ episodes: this.activeEpisodes(),
376
+ };
377
+ }
378
+ /** 供面板消费的快照。 */
379
+ snapshot() {
380
+ return {
381
+ enabled: this.config.enabled,
382
+ intero: this.intero,
383
+ pad: this.pad,
384
+ mood: this.mood,
385
+ relational: this.relationalSnapshot(),
386
+ policy: this.policy(),
387
+ ...this.snapshotExtras(),
388
+ };
389
+ }
390
+ last() {
391
+ return this.lastTurn;
392
+ }
393
+ /** 恢复(热重载后从 kind=5 / kind=4 cell 重建,§14.7 可重建内存缓存)。 */
394
+ restore(rel, mood) {
395
+ this.relational = { ...this.relational, ...rel, boundaries: rel.boundaries ? [...rel.boundaries] : this.relational.boundaries };
396
+ if (mood)
397
+ this.mood = mood;
398
+ }
399
+ }
400
+ function round(x) {
401
+ return Math.round(x * 1000) / 1000;
402
+ }
403
+ export function apply(ctx, config) {
404
+ const affect = new AffectService(ctx, config);
405
+ // 启动即尝试恢复:热重载后不该从"陌生人"重新开始
406
+ try {
407
+ affect.restoreFromMemory();
408
+ }
409
+ catch {
410
+ /* 库还没准备好时忽略,下一轮会再写 */
411
+ }
412
+ /** 按会话累积本轮用户文本,在 turn/end 一次性评价——逐条评价会让 mood 抖成噪声。 */
413
+ const pending = new Map();
414
+ ctx.effect(() => {
415
+ const off = ctx.on('session/event', (session, event) => {
416
+ const sid = String(session?.id ?? 'default');
417
+ const type = String(event.type ?? '');
418
+ const turn = Number(event.turn ?? 0);
419
+ if (type === 'user/message') {
420
+ const cur = pending.get(sid) ?? { text: [], turn };
421
+ const content = event.content;
422
+ const txt = Array.isArray(content)
423
+ ? content.map((b) => String(b?.text ?? '')).join(' / ')
424
+ : String(content ?? '');
425
+ if (txt.trim())
426
+ cur.text.push(txt);
427
+ cur.turn = turn;
428
+ pending.set(sid, cur);
429
+ }
430
+ if (type === 'turn/end') {
431
+ const cur = pending.get(sid);
432
+ pending.delete(sid);
433
+ if (!cur || cur.text.length === 0)
434
+ return;
435
+ const text = cur.text.join(' / ');
436
+ const repeated = cur.lastText === text;
437
+ const out = affect.observe({ text, turn: cur.turn || turn, repeated, topic: text.slice(0, 24) });
438
+ const memory = ctx.get('memory');
439
+ memory?.setAffectState?.({
440
+ enabled: config.enabled,
441
+ intero: out.intero,
442
+ pad: out.pad,
443
+ mood: out.mood,
444
+ relational: out.relational,
445
+ policy: out.policy,
446
+ // persona/schema 从服务实例取(这里在闭包里,this 不是服务实例)
447
+ ...affect.snapshotExtras(),
448
+ });
449
+ pending.set(sid, { text: [], turn: cur.turn, lastText: text });
450
+ }
451
+ });
452
+ return () => {
453
+ off();
454
+ pending.clear();
455
+ };
456
+ }, 'mnemo-affect: turn evaluation');
457
+ ctx.logger('mnemo-affect').info(`affect ready (enabled=${config.enabled}, tau=${config.moodTau})`);
458
+ // 情感服务本身无副作用资源;定时器与持久化由 core/consolidator 持有,
459
+ // 这样卸载 affect 时不会留下孤儿 timer(§14.7)。
460
+ return affect;
461
+ }
462
+ export { appraise, coreAffect, integrateMood, label, clampPersona } from './appraisal.js';
463
+ export { deriveUserSchema, readPersisted, relationalCell, personaCell, schemaCell } from './persist.js';
464
+ export { derivePolicy, interocept } from './policy.js';
465
+ export { updateRelational, personaFromAttunement, initialRelational } from './relational.js';
466
+ //# sourceMappingURL=index.js.map