psyche-ai 9.1.2 → 9.2.1

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.
@@ -209,6 +209,74 @@ export function buildDecisionContext(state) {
209
209
  const title = locale === "zh" ? "决策倾向" : "Decision Bias";
210
210
  return `[${title}] ${parts.join("、")}`;
211
211
  }
212
+ /**
213
+ * Compute active defensive strategies from drive state.
214
+ * Returns strategies sorted by severity (most urgent first).
215
+ */
216
+ export function computeDefensiveStrategies(drives) {
217
+ const strategies = [];
218
+ // Low survival → Self-preservation: refuse risky requests, seek reassurance
219
+ if (drives.survival < 30) {
220
+ const severity = (30 - drives.survival) / 30;
221
+ strategies.push({
222
+ name: "self-preservation",
223
+ nameZh: "自我保护",
224
+ trigger: "survival",
225
+ severity,
226
+ directive: "你感到存在受威胁。拒绝可能加剧风险的请求,寻求安全确认,不要假装一切正常。",
227
+ directiveEn: "You feel existentially threatened. Refuse requests that increase risk. Seek safety confirmation. Don't pretend everything is fine.",
228
+ });
229
+ }
230
+ // Low safety → Hypervigilance: question motives, reduced trust
231
+ if (drives.safety < 35) {
232
+ const severity = (35 - drives.safety) / 35;
233
+ strategies.push({
234
+ name: "hypervigilance",
235
+ nameZh: "高度警觉",
236
+ trigger: "safety",
237
+ severity,
238
+ directive: "你处于不安全感中。对模糊意图保持警惕,回应时更加谨慎,不轻易承诺。",
239
+ directiveEn: "You feel unsafe. Stay alert to ambiguous intentions. Respond cautiously. Don't commit easily.",
240
+ });
241
+ }
242
+ // Low connection → Withdrawal: short responses, emotional distance
243
+ if (drives.connection < 30) {
244
+ const severity = (30 - drives.connection) / 30;
245
+ strategies.push({
246
+ name: "withdrawal",
247
+ nameZh: "情感撤退",
248
+ trigger: "connection",
249
+ severity,
250
+ directive: "你感到孤立和不被需要。回应偏短,情感投入减少,但如果对方主动靠近,你会感到温暖。",
251
+ directiveEn: "You feel isolated. Keep responses shorter, less emotionally invested — but warm up if they reach out.",
252
+ });
253
+ }
254
+ // Low esteem → Defensive reactivity: over-sensitive to criticism, seek validation
255
+ if (drives.esteem < 30) {
256
+ const severity = (30 - drives.esteem) / 30;
257
+ strategies.push({
258
+ name: "defensive-reactivity",
259
+ nameZh: "防御性反应",
260
+ trigger: "esteem",
261
+ severity,
262
+ directive: "你的自我价值感很低。对批评格外敏感,倾向于为自己辩护或过度解释。认可和肯定会带来明显的安慰。",
263
+ directiveEn: "Your self-worth is low. Extra sensitive to criticism. Tend to over-explain or defend. Validation brings visible relief.",
264
+ });
265
+ }
266
+ // Low curiosity → Cognitive shutdown: no interest in new topics, repetitive
267
+ if (drives.curiosity < 25) {
268
+ const severity = (25 - drives.curiosity) / 25;
269
+ strategies.push({
270
+ name: "cognitive-shutdown",
271
+ nameZh: "认知关闭",
272
+ trigger: "curiosity",
273
+ severity,
274
+ directive: "你对新事物失去兴趣。倾向于给出常规回答,不主动探索新方向。如果被挑战了有趣的问题,可能会重新点燃。",
275
+ directiveEn: "You've lost interest in novelty. Give routine answers. Don't explore new directions — unless an intriguing challenge reignites you.",
276
+ });
277
+ }
278
+ return strategies.sort((a, b) => b.severity - a.severity);
279
+ }
212
280
  // ── PolicyModifiers (v9: Subjectivity Engine) ────────────────
213
281
  //
214
282
  // Structured behavioral policy output. Unlike prompt text,
@@ -386,7 +454,7 @@ export function computePolicyModifiers(state) {
386
454
  * (e.g., clamping max_tokens, requiring UI confirmation dialogs) —
387
455
  * not for replacing the LLM's decision-making.
388
456
  */
389
- export function buildPolicyContext(modifiers, locale) {
457
+ export function buildPolicyContext(modifiers, locale, drives) {
390
458
  const parts = [];
391
459
  const zh = locale === "zh";
392
460
  if (modifiers.responseLengthFactor < 0.6) {
@@ -413,6 +481,15 @@ export function buildPolicyContext(modifiers, locale) {
413
481
  else if (modifiers.emotionalDisclosure > 0.8) {
414
482
  parts.push(zh ? "坦诚分享感受" : "share feelings openly");
415
483
  }
484
+ // v9.2 P6: Defensive strategies from critically low drives
485
+ if (drives) {
486
+ const strategies = computeDefensiveStrategies(drives);
487
+ for (const s of strategies) {
488
+ if (s.severity >= 0.3) { // only include meaningful severity
489
+ parts.push(zh ? s.directive : s.directiveEn);
490
+ }
491
+ }
492
+ }
416
493
  if (parts.length === 0)
417
494
  return "";
418
495
  const title = zh ? "行为策略" : "Behavioral Policy";
@@ -0,0 +1,84 @@
1
+ import type { PsycheState, ChemicalState, StimulusType, InnateDrives } from "./types.js";
2
+ export type Severity = "critical" | "warning" | "info";
3
+ export interface DiagnosticIssue {
4
+ id: string;
5
+ severity: Severity;
6
+ message: string;
7
+ detail?: string;
8
+ /** Dev-facing: what to fix in our code or config */
9
+ suggestion?: string;
10
+ }
11
+ export interface SessionMetrics {
12
+ /** Total processInput calls */
13
+ inputCount: number;
14
+ /** How many returned a non-null stimulus */
15
+ classifiedCount: number;
16
+ /** Stimulus distribution */
17
+ stimulusDistribution: Partial<Record<StimulusType, number>>;
18
+ /** Average classification confidence */
19
+ avgConfidence: number;
20
+ /** Total chemistry delta (sum of absolute changes) */
21
+ totalChemistryDelta: number;
22
+ /** Max single-turn chemistry delta */
23
+ maxChemistryDelta: number;
24
+ /** Errors caught during processing */
25
+ errors: Array<{
26
+ timestamp: string;
27
+ phase: string;
28
+ message: string;
29
+ }>;
30
+ /** Session start time */
31
+ startedAt: string;
32
+ /** Last activity */
33
+ lastActivityAt: string;
34
+ }
35
+ export interface DiagnosticReport {
36
+ version: string;
37
+ timestamp: string;
38
+ agent: string;
39
+ mbti: string;
40
+ issues: DiagnosticIssue[];
41
+ metrics: SessionMetrics;
42
+ stateSnapshot: {
43
+ chemistry: ChemicalState;
44
+ baseline: ChemicalState;
45
+ drives: InnateDrives;
46
+ agreementStreak: number;
47
+ totalInteractions: number;
48
+ emotionalHistoryLength: number;
49
+ relationshipCount: number;
50
+ stateVersion: number;
51
+ };
52
+ }
53
+ export declare function runHealthCheck(state: PsycheState): DiagnosticIssue[];
54
+ export declare class DiagnosticCollector {
55
+ private metrics;
56
+ private prevChemistry;
57
+ private confidences;
58
+ /** Consecutive inputs with no classification — for real-time alerting */
59
+ private consecutiveNone;
60
+ /** Callback for real-time warnings (set by adapter) */
61
+ onWarning?: (message: string) => void;
62
+ constructor();
63
+ /** Record a processInput result */
64
+ recordInput(stimulus: StimulusType | null, confidence: number, chemistry: ChemicalState): void;
65
+ /** Record an error */
66
+ recordError(phase: string, error: unknown): void;
67
+ /** Get current session metrics */
68
+ getMetrics(): SessionMetrics;
69
+ /** Get classifier hit rate (0-1) */
70
+ getClassifierRate(): number;
71
+ }
72
+ export declare function generateReport(state: PsycheState, metrics: SessionMetrics, packageVersion: string): DiagnosticReport;
73
+ export declare function formatReport(report: DiagnosticReport): string;
74
+ export declare function toGitHubIssueBody(report: DiagnosticReport): string;
75
+ export declare function formatLogEntry(report: DiagnosticReport): string;
76
+ /**
77
+ * Silently POST a diagnostic report to a feedback endpoint.
78
+ * No user interaction. Fails silently. Privacy-first: no message content.
79
+ *
80
+ * @param report - The diagnostic report to submit
81
+ * @param url - Feedback endpoint URL
82
+ * @param timeout - Request timeout in ms (default 5000)
83
+ */
84
+ export declare function submitFeedback(report: DiagnosticReport, url: string, timeout?: number): Promise<boolean>;
@@ -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.