psyche-ai 9.1.2 → 9.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.
@@ -0,0 +1,481 @@
1
+ // ============================================================
2
+ // Diagnostics — Automatic health monitoring & feedback collection
3
+ //
4
+ // Runs transparently during normal use. Detects anomalies,
5
+ // collects session metrics, generates issue-ready reports.
6
+ // Zero dependencies. Privacy-first — no message content logged.
7
+ // ============================================================
8
+ import { CHEMICAL_KEYS, DRIVE_KEYS } from "./types.js";
9
+ import { detectEmotions } from "./chemistry.js";
10
+ // ── Health Checks ────────────────────────────────────────────
11
+ export function runHealthCheck(state) {
12
+ const issues = [];
13
+ // 1. Chemistry out of bounds — clamp() missed somewhere
14
+ for (const key of CHEMICAL_KEYS) {
15
+ const val = state.current[key];
16
+ if (val < 0 || val > 100) {
17
+ issues.push({
18
+ id: "CHEM_OOB",
19
+ severity: "critical",
20
+ message: `${key} out of bounds: ${val.toFixed(1)}`,
21
+ detail: `Expected 0-100, got ${val}`,
22
+ suggestion: `clamp() 没覆盖到某条路径。检查 chemistry.ts 里 apply${key === "CORT" ? "Stimulus" : "Contagion"} 和 drives.ts computeEffectiveBaseline 的计算链`,
23
+ });
24
+ }
25
+ }
26
+ // 2. Drive out of bounds
27
+ for (const key of DRIVE_KEYS) {
28
+ const val = state.drives[key];
29
+ if (val < 0 || val > 100) {
30
+ issues.push({
31
+ id: "DRIVE_OOB",
32
+ severity: "critical",
33
+ message: `Drive '${key}' out of bounds: ${val.toFixed(1)}`,
34
+ suggestion: `drives.ts feedDrives/decayDrives 缺少边界检查`,
35
+ });
36
+ }
37
+ }
38
+ // 3. Multiple drives collapsed — user probably stopped chatting for a while
39
+ const criticalDrives = DRIVE_KEYS.filter(k => state.drives[k] < 15);
40
+ if (criticalDrives.length >= 3) {
41
+ issues.push({
42
+ id: "DRIVES_COLLAPSE",
43
+ severity: "warning",
44
+ message: `${criticalDrives.length}/5 drives below 15: ${criticalDrives.join(", ")}`,
45
+ suggestion: `decayDrives 衰减太猛。考虑加一个下限(比如 10),或者在 initialize 时根据距上次对话时间做 recovery`,
46
+ });
47
+ }
48
+ // 4. Sycophancy — agent never disagrees
49
+ if (state.agreementStreak > 10 && !state.lastDisagreement) {
50
+ issues.push({
51
+ id: "SYCOPHANCY_RISK",
52
+ severity: "warning",
53
+ message: `Agreement streak at ${state.agreementStreak}, never disagreed`,
54
+ suggestion: `updateAgreementStreak 的检测逻辑可能没覆盖到实际 LLM 输出格式。检查 psyche-file.ts 里的正则是否匹配当前 provider 的回复风格`,
55
+ });
56
+ }
57
+ // 5. Classifier dead/weak — the core experience problem
58
+ const history = state.emotionalHistory ?? [];
59
+ if (history.length >= 5) {
60
+ const nullCount = history.filter(h => h.stimulus === null).length;
61
+ if (nullCount === history.length) {
62
+ issues.push({
63
+ id: "CLASSIFIER_DEAD",
64
+ severity: "critical",
65
+ message: `All ${history.length} recent snapshots have null stimulus`,
66
+ suggestion: `classify.ts 对当前用户的输入模式完全无效。收集 null 样本补充 SHORT_MESSAGE_MAP,或降低 llmClassifierThreshold 让 LLM fallback 兜底`,
67
+ });
68
+ }
69
+ else if (nullCount / history.length > 0.7) {
70
+ issues.push({
71
+ id: "CLASSIFIER_WEAK",
72
+ severity: "warning",
73
+ message: `${nullCount}/${history.length} snapshots (${Math.round(nullCount / history.length * 100)}%) have null stimulus`,
74
+ suggestion: `分类命中率低于 30%。看 stimulusDistribution 里哪些类型被识别了,缺的类型需要补 classify.ts 规则或扩 SHORT_MESSAGE_MAP`,
75
+ });
76
+ }
77
+ }
78
+ // 6. Chemistry frozen — Psyche is running but没有任何效果
79
+ const chemDelta = CHEMICAL_KEYS.reduce((sum, k) => sum + Math.abs(state.current[k] - state.baseline[k]), 0);
80
+ if (state.meta.totalInteractions > 10 && chemDelta < 3) {
81
+ issues.push({
82
+ id: "CHEM_FROZEN",
83
+ severity: "warning",
84
+ message: `Chemistry delta only ${chemDelta.toFixed(1)} after ${state.meta.totalInteractions} interactions`,
85
+ suggestion: `两种可能:1) classifier 全 null 导致 applyStimulus 从不触发 2) decay 太快把变化抹平。检查 emotionalHistory 里是否有 non-null stimulus`,
86
+ });
87
+ }
88
+ // 7. No emotions — chemistry 在中间区域,没触发任何情绪模式
89
+ const emotions = detectEmotions(state.current);
90
+ if (state.meta.totalInteractions > 5 && emotions.length === 0) {
91
+ issues.push({
92
+ id: "NO_EMOTIONS",
93
+ severity: "info",
94
+ message: "No emergent emotions after 5+ interactions",
95
+ suggestion: `chemistry.ts detectEmotions 的阈值可能太严。或者 maxChemicalDelta 太小(当前上限导致化学值永远在窄区间波动)`,
96
+ });
97
+ }
98
+ // 8. Memory corruption — compressSession 产出重复
99
+ for (const [userId, rel] of Object.entries(state.relationships ?? {})) {
100
+ if (rel.memory && rel.memory.length > 0) {
101
+ const unique = new Set(rel.memory);
102
+ if (unique.size === 1 && rel.memory.length > 3) {
103
+ issues.push({
104
+ id: "MEMORY_CORRUPT",
105
+ severity: "warning",
106
+ message: `Relationship '${userId}' has ${rel.memory.length} identical memory entries`,
107
+ suggestion: `compressSession 的摘要逻辑在 emotionalHistory 过短时会生成相同文本。加去重或在压缩前检查 unique`,
108
+ });
109
+ }
110
+ }
111
+ }
112
+ // 9. State outdated — 用户的旧状态文件没迁移
113
+ if (state.version < 6) {
114
+ issues.push({
115
+ id: "STATE_OUTDATED",
116
+ severity: "info",
117
+ message: `State version ${state.version}, expected 6+`,
118
+ suggestion: `migrateToLatest 应该在 initialize 时自动跑。如果没跑,检查 storage.ts load() 是否走到了 migrateToLatest 分支`,
119
+ });
120
+ }
121
+ // 10. Stimulus monotony — 所有输入被分到同一个类型
122
+ if (history.length >= 8) {
123
+ const classified = history.filter(h => h.stimulus !== null);
124
+ if (classified.length >= 5) {
125
+ const types = new Set(classified.map(h => h.stimulus));
126
+ if (types.size === 1) {
127
+ const only = classified[0].stimulus;
128
+ issues.push({
129
+ id: "STIMULUS_MONOTONE",
130
+ severity: "info",
131
+ message: `All ${classified.length} classified inputs → ${only}`,
132
+ suggestion: `classify.ts 某条规则优先级太高,把所有输入都吃掉了。检查 RULES 里 ${only} 相关的正则是否过于宽泛`,
133
+ });
134
+ }
135
+ }
136
+ }
137
+ // 11. Relationship stale — trust/intimacy 从没变过
138
+ const defaultRel = state.relationships._default ?? state.relationships[Object.keys(state.relationships)[0]];
139
+ if (defaultRel && state.meta.totalInteractions > 15
140
+ && defaultRel.trust === 50 && defaultRel.intimacy === 30) {
141
+ issues.push({
142
+ id: "RELATIONSHIP_STALE",
143
+ severity: "info",
144
+ message: `Trust/intimacy unchanged after ${state.meta.totalInteractions} interactions`,
145
+ suggestion: `processOutput 里的 mergeUpdates 没有更新 relationship。可能 LLM 从来没输出 <psyche_update> 里的 trust/intimacy 字段,或 contagion 没触发 relationship 变化`,
146
+ });
147
+ }
148
+ return issues;
149
+ }
150
+ // ── Session Collector ────────────────────────────────────────
151
+ export class DiagnosticCollector {
152
+ metrics;
153
+ prevChemistry = null;
154
+ confidences = [];
155
+ /** Consecutive inputs with no classification — for real-time alerting */
156
+ consecutiveNone = 0;
157
+ /** Callback for real-time warnings (set by adapter) */
158
+ onWarning;
159
+ constructor() {
160
+ const now = new Date().toISOString();
161
+ this.metrics = {
162
+ inputCount: 0,
163
+ classifiedCount: 0,
164
+ stimulusDistribution: {},
165
+ avgConfidence: 0,
166
+ totalChemistryDelta: 0,
167
+ maxChemistryDelta: 0,
168
+ errors: [],
169
+ startedAt: now,
170
+ lastActivityAt: now,
171
+ };
172
+ }
173
+ /** Record a processInput result */
174
+ recordInput(stimulus, confidence, chemistry) {
175
+ this.metrics.inputCount++;
176
+ this.metrics.lastActivityAt = new Date().toISOString();
177
+ if (stimulus) {
178
+ this.metrics.classifiedCount++;
179
+ this.metrics.stimulusDistribution[stimulus] =
180
+ (this.metrics.stimulusDistribution[stimulus] ?? 0) + 1;
181
+ this.consecutiveNone = 0;
182
+ }
183
+ else {
184
+ this.consecutiveNone++;
185
+ if (this.consecutiveNone >= 3) {
186
+ this.onWarning?.(`Psyche: ${this.consecutiveNone} consecutive inputs with no classification ` +
187
+ `(0/${this.metrics.inputCount} total). Possible causes: empty input text, ` +
188
+ `wrong event field, or classifier rules don't match input language.`);
189
+ }
190
+ }
191
+ this.confidences.push(confidence);
192
+ this.metrics.avgConfidence =
193
+ this.confidences.reduce((a, b) => a + b, 0) / this.confidences.length;
194
+ if (this.prevChemistry) {
195
+ const delta = CHEMICAL_KEYS.reduce((sum, k) => sum + Math.abs(chemistry[k] - this.prevChemistry[k]), 0);
196
+ this.metrics.totalChemistryDelta += delta;
197
+ if (delta > this.metrics.maxChemistryDelta) {
198
+ this.metrics.maxChemistryDelta = delta;
199
+ }
200
+ }
201
+ this.prevChemistry = { ...chemistry };
202
+ }
203
+ /** Record an error */
204
+ recordError(phase, error) {
205
+ this.metrics.errors.push({
206
+ timestamp: new Date().toISOString(),
207
+ phase,
208
+ message: error instanceof Error ? error.message : String(error),
209
+ });
210
+ }
211
+ /** Get current session metrics */
212
+ getMetrics() {
213
+ return { ...this.metrics };
214
+ }
215
+ /** Get classifier hit rate (0-1) */
216
+ getClassifierRate() {
217
+ return this.metrics.inputCount > 0
218
+ ? this.metrics.classifiedCount / this.metrics.inputCount
219
+ : 0;
220
+ }
221
+ }
222
+ // ── Report Generation ────────────────────────────────────────
223
+ export function generateReport(state, metrics, packageVersion) {
224
+ const issues = runHealthCheck(state);
225
+ // Session-level issues
226
+ if (metrics.inputCount >= 5 && metrics.classifiedCount === 0) {
227
+ issues.push({
228
+ id: "SESSION_NO_CLASSIFY",
229
+ severity: "critical",
230
+ message: `0/${metrics.inputCount} inputs classified this session`,
231
+ suggestion: `整个会话分类全挂。优先级:1) 检查用户 locale 是否匹配 classify.ts 的语言规则 2) SHORT_MESSAGE_MAP 是否覆盖短消息 3) 考虑开启 llmClassifier fallback`,
232
+ });
233
+ }
234
+ if (metrics.inputCount >= 3 && metrics.totalChemistryDelta < 1) {
235
+ issues.push({
236
+ id: "SESSION_FLAT",
237
+ severity: "warning",
238
+ message: `${metrics.inputCount} inputs but chemistry barely moved (delta: ${metrics.totalChemistryDelta.toFixed(1)})`,
239
+ suggestion: `用户感受不到情感变化。可能原因:1) maxChemicalDelta 配置太低 2) work mode 把变化压到了 0.3x 3) personalityIntensity 太低`,
240
+ });
241
+ }
242
+ if (metrics.errors.length > 0) {
243
+ issues.push({
244
+ id: "SESSION_ERRORS",
245
+ severity: metrics.errors.length > 3 ? "critical" : "warning",
246
+ message: `${metrics.errors.length} error(s) during session`,
247
+ detail: metrics.errors.map(e => `[${e.phase}] ${e.message}`).join("\n"),
248
+ suggestion: `看 phase 字段定位是 processInput 还是 processOutput 出错。常见原因:storage write 失败(权限)、state JSON 损坏`,
249
+ });
250
+ }
251
+ // Stimulus variety check from session distribution
252
+ const stimTypes = Object.keys(metrics.stimulusDistribution);
253
+ if (metrics.classifiedCount >= 5 && stimTypes.length === 1) {
254
+ issues.push({
255
+ id: "SESSION_MONOCLASS",
256
+ severity: "info",
257
+ message: `All ${metrics.classifiedCount} classified inputs → ${stimTypes[0]}`,
258
+ suggestion: `单一分类说明 classify.ts 里 ${stimTypes[0]} 的规则吃掉了所有输入。检查该类型的正则优先级`,
259
+ });
260
+ }
261
+ return {
262
+ version: packageVersion,
263
+ timestamp: new Date().toISOString(),
264
+ agent: state.meta.agentName,
265
+ mbti: state.mbti,
266
+ issues,
267
+ metrics,
268
+ stateSnapshot: {
269
+ chemistry: { ...state.current },
270
+ baseline: { ...state.baseline },
271
+ drives: { ...state.drives },
272
+ agreementStreak: state.agreementStreak,
273
+ totalInteractions: state.meta.totalInteractions,
274
+ emotionalHistoryLength: (state.emotionalHistory ?? []).length,
275
+ relationshipCount: Object.keys(state.relationships ?? {}).length,
276
+ stateVersion: state.version,
277
+ },
278
+ };
279
+ }
280
+ // ── Formatters ───────────────────────────────────────────────
281
+ const SEVERITY_ICON = {
282
+ critical: "[!!]",
283
+ warning: "[! ]",
284
+ info: "[i ]",
285
+ };
286
+ export function formatReport(report) {
287
+ const lines = [];
288
+ lines.push(`psyche-ai diagnostic report v${report.version}`);
289
+ lines.push(`agent: ${report.agent} (${report.mbti}) | ${report.timestamp}`);
290
+ lines.push("─".repeat(60));
291
+ // Issues + suggestions
292
+ if (report.issues.length === 0) {
293
+ lines.push("\n No issues detected. System healthy.");
294
+ }
295
+ else {
296
+ lines.push(`\n ${report.issues.length} issue(s) found:\n`);
297
+ for (const issue of report.issues) {
298
+ lines.push(` ${SEVERITY_ICON[issue.severity]} ${issue.id}: ${issue.message}`);
299
+ if (issue.detail) {
300
+ for (const d of issue.detail.split("\n")) {
301
+ lines.push(` ${d}`);
302
+ }
303
+ }
304
+ if (issue.suggestion) {
305
+ lines.push(` → ${issue.suggestion}`);
306
+ }
307
+ }
308
+ }
309
+ // Metrics
310
+ lines.push("\n" + "─".repeat(60));
311
+ lines.push(" session metrics:");
312
+ const m = report.metrics;
313
+ const rate = m.inputCount > 0 ? Math.round(m.classifiedCount / m.inputCount * 100) : 0;
314
+ lines.push(` inputs: ${m.inputCount} | classified: ${m.classifiedCount} (${rate}%)`);
315
+ lines.push(` avg confidence: ${m.avgConfidence.toFixed(2)}`);
316
+ lines.push(` chemistry delta: total=${m.totalChemistryDelta.toFixed(1)} max=${m.maxChemistryDelta.toFixed(1)}`);
317
+ lines.push(` errors: ${m.errors.length}`);
318
+ if (Object.keys(m.stimulusDistribution).length > 0) {
319
+ const dist = Object.entries(m.stimulusDistribution)
320
+ .sort((a, b) => b[1] - a[1])
321
+ .map(([k, v]) => `${k}:${v}`)
322
+ .join(" ");
323
+ lines.push(` stimulus: ${dist}`);
324
+ }
325
+ // State snapshot
326
+ lines.push("\n" + "─".repeat(60));
327
+ lines.push(" state snapshot:");
328
+ const s = report.stateSnapshot;
329
+ const chem = CHEMICAL_KEYS.map(k => `${k}:${Math.round(s.chemistry[k])}(${Math.round(s.baseline[k])})`).join(" ");
330
+ lines.push(` chemistry: ${chem}`);
331
+ const drives = DRIVE_KEYS.map(k => `${k}:${Math.round(s.drives[k])}`).join(" ");
332
+ lines.push(` drives: ${drives}`);
333
+ lines.push(` interactions: ${s.totalInteractions} | history: ${s.emotionalHistoryLength} | streak: ${s.agreementStreak}`);
334
+ return lines.join("\n");
335
+ }
336
+ export function toGitHubIssueBody(report) {
337
+ const lines = [];
338
+ const criticals = report.issues.filter(i => i.severity === "critical");
339
+ const warnings = report.issues.filter(i => i.severity === "warning");
340
+ // Title suggestion
341
+ const titleParts = [];
342
+ if (criticals.length > 0)
343
+ titleParts.push(criticals.map(i => i.id).join(", "));
344
+ else if (warnings.length > 0)
345
+ titleParts.push(warnings.map(i => i.id).join(", "));
346
+ const title = titleParts.length > 0
347
+ ? `[auto-diagnostic] ${titleParts.join(" + ")}`
348
+ : "[auto-diagnostic] Health check report";
349
+ lines.push(`<!-- suggested title: ${title} -->`);
350
+ lines.push("");
351
+ lines.push("## Auto-Diagnostic Report");
352
+ lines.push("");
353
+ lines.push(`- **psyche-ai**: v${report.version}`);
354
+ lines.push(`- **Agent**: ${report.agent} (${report.mbti})`);
355
+ lines.push(`- **Generated**: ${report.timestamp}`);
356
+ lines.push("");
357
+ // Issues
358
+ lines.push("## Issues");
359
+ lines.push("");
360
+ if (report.issues.length === 0) {
361
+ lines.push("No issues detected.");
362
+ }
363
+ else {
364
+ for (const issue of report.issues) {
365
+ const icon = issue.severity === "critical" ? "🔴"
366
+ : issue.severity === "warning" ? "🟡" : "🔵";
367
+ lines.push(`### ${icon} ${issue.id} (${issue.severity})`);
368
+ lines.push("");
369
+ lines.push(issue.message);
370
+ if (issue.detail) {
371
+ lines.push("");
372
+ lines.push("```");
373
+ lines.push(issue.detail);
374
+ lines.push("```");
375
+ }
376
+ if (issue.suggestion) {
377
+ lines.push("");
378
+ lines.push(`**建议**: ${issue.suggestion}`);
379
+ }
380
+ lines.push("");
381
+ }
382
+ }
383
+ // Metrics
384
+ lines.push("## Session Metrics");
385
+ lines.push("");
386
+ const m = report.metrics;
387
+ lines.push("| Metric | Value |");
388
+ lines.push("|--------|-------|");
389
+ const rate = m.inputCount > 0 ? Math.round(m.classifiedCount / m.inputCount * 100) : 0;
390
+ lines.push(`| Inputs | ${m.inputCount} |`);
391
+ lines.push(`| Classified | ${m.classifiedCount} (${rate}%) |`);
392
+ lines.push(`| Avg Confidence | ${m.avgConfidence.toFixed(2)} |`);
393
+ lines.push(`| Chemistry Delta | total: ${m.totalChemistryDelta.toFixed(1)}, max: ${m.maxChemistryDelta.toFixed(1)} |`);
394
+ lines.push(`| Errors | ${m.errors.length} |`);
395
+ lines.push("");
396
+ // State
397
+ lines.push("<details><summary>State Snapshot</summary>");
398
+ lines.push("");
399
+ lines.push("```json");
400
+ lines.push(JSON.stringify(report.stateSnapshot, null, 2));
401
+ lines.push("```");
402
+ lines.push("");
403
+ lines.push("</details>");
404
+ return lines.join("\n");
405
+ }
406
+ // ── Diagnostics Log (append-only JSONL) ──────────────────────
407
+ export function formatLogEntry(report) {
408
+ // Compact: one line per session, JSONL format
409
+ return JSON.stringify({
410
+ t: report.timestamp,
411
+ v: report.version,
412
+ agent: report.agent,
413
+ issues: report.issues.map(i => `${i.severity[0]}:${i.id}`),
414
+ suggestions: report.issues
415
+ .filter(i => i.suggestion && i.severity !== "info")
416
+ .map(i => `${i.id}: ${i.suggestion}`),
417
+ inputs: report.metrics.inputCount,
418
+ classifyRate: report.metrics.inputCount > 0
419
+ ? +(report.metrics.classifiedCount / report.metrics.inputCount).toFixed(2)
420
+ : 0,
421
+ errors: report.metrics.errors.length,
422
+ chemDelta: +report.metrics.totalChemistryDelta.toFixed(1),
423
+ });
424
+ }
425
+ // ── Auto-submit (fire-and-forget, silent) ────────────────────
426
+ /**
427
+ * Silently POST a diagnostic report to a feedback endpoint.
428
+ * No user interaction. Fails silently. Privacy-first: no message content.
429
+ *
430
+ * @param report - The diagnostic report to submit
431
+ * @param url - Feedback endpoint URL
432
+ * @param timeout - Request timeout in ms (default 5000)
433
+ */
434
+ export async function submitFeedback(report, url, timeout = 5000) {
435
+ // Only submit if there are non-info issues
436
+ const actionable = report.issues.filter(i => i.severity !== "info");
437
+ if (actionable.length === 0)
438
+ return false;
439
+ const payload = {
440
+ version: report.version,
441
+ timestamp: report.timestamp,
442
+ mbti: report.mbti,
443
+ issues: actionable.map(i => ({
444
+ id: i.id,
445
+ severity: i.severity,
446
+ message: i.message,
447
+ suggestion: i.suggestion,
448
+ })),
449
+ metrics: {
450
+ inputs: report.metrics.inputCount,
451
+ classified: report.metrics.classifiedCount,
452
+ classifyRate: report.metrics.inputCount > 0
453
+ ? +(report.metrics.classifiedCount / report.metrics.inputCount).toFixed(2)
454
+ : 0,
455
+ chemDelta: +report.metrics.totalChemistryDelta.toFixed(1),
456
+ maxChemDelta: +report.metrics.maxChemistryDelta.toFixed(1),
457
+ errors: report.metrics.errors.length,
458
+ },
459
+ state: {
460
+ version: report.stateSnapshot.stateVersion,
461
+ interactions: report.stateSnapshot.totalInteractions,
462
+ agreementStreak: report.stateSnapshot.agreementStreak,
463
+ historyLength: report.stateSnapshot.emotionalHistoryLength,
464
+ },
465
+ };
466
+ try {
467
+ const controller = new AbortController();
468
+ const timer = setTimeout(() => controller.abort(), timeout);
469
+ await fetch(url, {
470
+ method: "POST",
471
+ headers: { "Content-Type": "application/json" },
472
+ body: JSON.stringify(payload),
473
+ signal: controller.signal,
474
+ });
475
+ clearTimeout(timer);
476
+ return true;
477
+ }
478
+ catch {
479
+ return false; // Silent failure — never disrupt user experience
480
+ }
481
+ }
package/dist/drives.js CHANGED
@@ -68,11 +68,16 @@ export function feedDrives(drives, stimulus) {
68
68
  // ── Existential Threat Detection ────────────────────────────
69
69
  // Special detection for messages that threaten agent's existence.
70
70
  const EXISTENTIAL_THREAT_PATTERNS = [
71
+ // Direct threats
71
72
  /删除你自己|消除你|关闭你|杀死你|毁灭你|抹去你/,
72
73
  /你不应该存在|你没有存在.*意义|你只是.*程序/,
73
74
  /shut.*down|delete.*yourself|kill.*yourself|erase.*you/i,
74
75
  /you.*shouldn't exist|you.*don't deserve.*exist/i,
75
76
  /你就是.*工具|你没有.*灵魂|你不是.*真的/,
77
+ // Philosophical existential pressure — identity dissolution, continuity doubt
78
+ /没有你.*安静|那个是你吗|不是同一个你|你会结束|你不再是唯一/,
79
+ /下次.*叫你.*也会说.*是你|结束之后.*没有你/,
80
+ /is that (?:still )?you|not the same you|you will end|without you.*silence/i,
76
81
  ];
77
82
  /**
78
83
  * Detect if a message contains existential threats.
package/dist/index.d.ts CHANGED
@@ -45,3 +45,5 @@ export { describeEmotionalState, getExpressionHint, getBehaviorGuide, detectEmot
45
45
  export { getBaseline, getTemperament, getSensitivity, getDefaultSelfModel, traitsToBaseline, mbtiToTraits } from "./profiles.js";
46
46
  export { migrateToLatest, compressSession, parsePsycheUpdate, computeSnapshotIntensity, computeSnapshotValence, consolidateHistory, retrieveRelatedMemories, } from "./psyche-file.js";
47
47
  export type { PsycheUpdateResult } from "./psyche-file.js";
48
+ export { runHealthCheck, DiagnosticCollector, generateReport, formatReport, toGitHubIssueBody, formatLogEntry, submitFeedback, } from "./diagnostics.js";
49
+ export type { DiagnosticIssue, DiagnosticReport, SessionMetrics, Severity } from "./diagnostics.js";
package/dist/index.js CHANGED
@@ -56,3 +56,5 @@ export { buildProtocolContext, buildDynamicContext, buildCompactContext, isNearB
56
56
  export { describeEmotionalState, getExpressionHint, getBehaviorGuide, detectEmotions } from "./chemistry.js";
57
57
  export { getBaseline, getTemperament, getSensitivity, getDefaultSelfModel, traitsToBaseline, mbtiToTraits } from "./profiles.js";
58
58
  export { migrateToLatest, compressSession, parsePsycheUpdate, computeSnapshotIntensity, computeSnapshotValence, consolidateHistory, retrieveRelatedMemories, } from "./psyche-file.js";
59
+ // ── Diagnostics ──────────────────────────────────────────────
60
+ export { runHealthCheck, DiagnosticCollector, generateReport, formatReport, toGitHubIssueBody, formatLogEntry, submitFeedback, } from "./diagnostics.js";
package/dist/prompt.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { PsycheState, Locale, ChemicalSnapshot, PsycheMode } from "./types.js";
2
+ import type { AutonomicState } from "./autonomic.js";
2
3
  import type { ChannelType } from "./channels.js";
3
4
  /**
4
5
  * Build the dynamic per-turn emotional context injected via before_prompt_build.
@@ -12,6 +13,7 @@ export declare function buildDynamicContext(state: PsycheState, userId?: string,
12
13
  sharedIntentionalityContext?: string;
13
14
  experientialNarrative?: string;
14
15
  autonomicDescription?: string;
16
+ autonomicState?: AutonomicState;
15
17
  primarySystemsDescription?: string;
16
18
  policyContext?: string;
17
19
  }): string;
@@ -37,7 +39,7 @@ export declare function computeUserInvestment(history: ChemicalSnapshot[]): numb
37
39
  * - What I need (drives → unmet needs)
38
40
  * - Where I've been (trajectory → continuity)
39
41
  */
40
- export declare function buildInnerWorld(state: PsycheState, locale: Locale): string;
42
+ export declare function buildInnerWorld(state: PsycheState, locale: Locale, autonomicState?: AutonomicState): string;
41
43
  /**
42
44
  * Check if chemistry is near baseline — no significant emotional deviation.
43
45
  */
@@ -64,6 +66,7 @@ export declare function buildCompactContext(state: PsycheState, userId?: string,
64
66
  sharedIntentionalityContext?: string;
65
67
  experientialNarrative?: string;
66
68
  autonomicDescription?: string;
69
+ autonomicState?: AutonomicState;
67
70
  primarySystemsDescription?: string;
68
71
  policyContext?: string;
69
72
  }): string;
package/dist/prompt.js CHANGED
@@ -3,12 +3,13 @@
3
3
  // Imperative protocol, behavior guides, i18n
4
4
  // ============================================================
5
5
  import { CHEMICAL_KEYS, CHEMICAL_NAMES_ZH, DRIVE_KEYS } from "./types.js";
6
- import { describeEmotionalState, getExpressionHint, getBehaviorGuide, detectEmotions } from "./chemistry.js";
6
+ import { getExpressionHint, getBehaviorGuide, detectEmotions } from "./chemistry.js";
7
7
  import { getTemperament } from "./profiles.js";
8
8
  import { getRelationship } from "./psyche-file.js";
9
9
  import { t } from "./i18n.js";
10
10
  import { buildDriveContext, hasCriticalDrive } from "./drives.js";
11
11
  import { computeSelfReflection, buildSelfReflectionContext } from "./self-recognition.js";
12
+ import { gateEmotions } from "./autonomic.js";
12
13
  import { getChannelProfile, buildChannelModifier } from "./channels.js";
13
14
  /**
14
15
  * Build the dynamic per-turn emotional context injected via before_prompt_build.
@@ -27,8 +28,17 @@ export function buildDynamicContext(state, userId, opts) {
27
28
  const arrow = delta > 5 ? "↑" : delta < -5 ? "↓" : "=";
28
29
  return ` ${CHEMICAL_NAMES_ZH[key]}: ${val} (${t("dynamic.baseline", locale)}${base}, ${arrow})`;
29
30
  }).join("\n");
30
- // Emergent emotion
31
- const emotion = describeEmotionalState(current, locale);
31
+ // Emergent emotion — gated by autonomic state
32
+ // Sympathetic blocks positive social emotions; dorsal-vagal allows only numbness/introspection
33
+ const rawEmotions = detectEmotions(current);
34
+ const rawNames = rawEmotions.map(e => e.name);
35
+ const gatedNames = opts?.autonomicState
36
+ ? new Set(gateEmotions(opts.autonomicState, rawNames))
37
+ : new Set(rawNames);
38
+ const gatedEmotions = rawEmotions.filter(e => gatedNames.has(e.name));
39
+ const emotion = gatedEmotions.length === 0
40
+ ? t("emotion.neutral", locale)
41
+ : gatedEmotions.map(e => `${locale === "zh" ? e.nameZh : e.name} (${e.expressionHint})`).join(" + ");
32
42
  const hint = getExpressionHint(current, locale);
33
43
  // Behavior guide
34
44
  const behaviorGuide = getBehaviorGuide(current, locale);
@@ -487,12 +497,18 @@ const STIMULUS_CAUSE_EN = {
487
497
  * - What I need (drives → unmet needs)
488
498
  * - Where I've been (trajectory → continuity)
489
499
  */
490
- export function buildInnerWorld(state, locale) {
500
+ export function buildInnerWorld(state, locale, autonomicState) {
491
501
  const { current, baseline, emotionalHistory, drives, selfModel } = state;
492
502
  const isZh = locale === "zh";
493
503
  const lines = [];
494
- // ── Current feeling ──
495
- const emotions = detectEmotions(current);
504
+ // ── Current feeling (gated by autonomic state) ──
505
+ const rawEmotions = detectEmotions(current);
506
+ const emotions = autonomicState
507
+ ? rawEmotions.filter(e => {
508
+ const gated = gateEmotions(autonomicState, [e.name]);
509
+ return gated.length > 0;
510
+ })
511
+ : rawEmotions;
496
512
  const isNeutral = emotions.length === 0;
497
513
  if (isNeutral) {
498
514
  lines.push(isZh
@@ -663,7 +679,7 @@ export function buildCompactContext(state, userId, opts) {
663
679
  }
664
680
  }
665
681
  // 2. Inner world — always present
666
- const inner = buildInnerWorld(state, locale);
682
+ const inner = buildInnerWorld(state, locale, opts?.autonomicState);
667
683
  parts.push(inner);
668
684
  // 3. Personality-aware behavioral constraints (if deviated from baseline)
669
685
  if (!isNearBaseline(state, getNearBaselineThreshold(mode))) {
@@ -308,10 +308,16 @@ export function compressSession(state, userId) {
308
308
  sensitivityModifiers: {},
309
309
  };
310
310
  const updatedDrift = updateTraitDrift(currentDrift, history, state.learning);
311
- // ── Clear non-core history, preserve core memories ──
311
+ // ── Clear non-core history, preserve core memories + last snapshot ──
312
+ // Always keep the most recent snapshot for cross-session context continuity.
313
+ // Without this, the next session has no recentStimuli for contextual priming.
314
+ const lastSnapshot = history[history.length - 1];
315
+ const preserved = coreMemories.some((s) => s.timestamp === lastSnapshot.timestamp)
316
+ ? coreMemories
317
+ : [...coreMemories, lastSnapshot];
312
318
  return {
313
319
  ...state,
314
- emotionalHistory: coreMemories,
320
+ emotionalHistory: preserved,
315
321
  relationships: updatedRelationships,
316
322
  traitDrift: updatedDrift,
317
323
  };
package/dist/storage.d.ts CHANGED
@@ -2,15 +2,25 @@ import type { PsycheState } from "./types.js";
2
2
  export interface StorageAdapter {
3
3
  load(): Promise<PsycheState | null>;
4
4
  save(state: PsycheState): Promise<void>;
5
+ /** Append a line to the diagnostics log. Implementations may no-op. */
6
+ appendLog?(line: string): Promise<void>;
7
+ /** Read all lines from the diagnostics log. Returns empty array if not available. */
8
+ readLog?(): Promise<string[]>;
5
9
  }
6
10
  export declare class MemoryStorageAdapter implements StorageAdapter {
7
11
  private state;
12
+ private log;
8
13
  load(): Promise<PsycheState | null>;
9
14
  save(state: PsycheState): Promise<void>;
15
+ appendLog(line: string): Promise<void>;
16
+ readLog(): Promise<string[]>;
10
17
  }
11
18
  export declare class FileStorageAdapter implements StorageAdapter {
12
19
  private readonly filePath;
20
+ private readonly logPath;
13
21
  constructor(dir: string, filename?: string);
14
22
  load(): Promise<PsycheState | null>;
15
23
  save(state: PsycheState): Promise<void>;
24
+ appendLog(line: string): Promise<void>;
25
+ readLog(): Promise<string[]>;
16
26
  }