psyche-ai 9.2.5 → 9.2.7

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.
@@ -1,4 +1,4 @@
1
- import type { PsycheState, MBTIType, ChemicalState, RelationshipState, Locale, StimulusType, ChemicalSnapshot } from "./types.js";
1
+ import type { PsycheState, MBTIType, ChemicalState, RelationshipState, Locale, StimulusType, ChemicalSnapshot, WritebackSignalType } from "./types.js";
2
2
  export interface SemanticTurnSummary {
3
3
  summary: string;
4
4
  points?: string[];
@@ -93,6 +93,10 @@ export interface PsycheUpdateResult {
93
93
  state: Partial<PsycheState>;
94
94
  /** LLM-assisted stimulus classification (when algorithm was uncertain) */
95
95
  llmStimulus?: StimulusType;
96
+ /** Sparse agent-authored writeback signals */
97
+ signals?: WritebackSignalType[];
98
+ /** Optional writeback confidence */
99
+ signalConfidence?: number;
96
100
  }
97
101
  /**
98
102
  * Parse a <psyche_update> block from LLM output.
@@ -710,10 +710,42 @@ export function parsePsycheUpdate(text, logger = NOOP_LOGGER) {
710
710
  llmStimulus = candidate;
711
711
  }
712
712
  }
713
+ const VALID_WRITEBACK_SIGNALS = new Set([
714
+ "trust_up",
715
+ "trust_down",
716
+ "boundary_set",
717
+ "boundary_soften",
718
+ "repair_attempt",
719
+ "repair_landed",
720
+ "closeness_invite",
721
+ "withdrawal_mark",
722
+ "self_assertion",
723
+ "task_recenter",
724
+ ]);
725
+ let signals;
726
+ const signalsMatch = block.match(/signals\s*[::]\s*([^\n]+)/i);
727
+ if (signalsMatch) {
728
+ const parsed = signalsMatch[1]
729
+ .split(/[,\s|]+/)
730
+ .map((item) => item.trim())
731
+ .filter(Boolean)
732
+ .filter((item) => VALID_WRITEBACK_SIGNALS.has(item));
733
+ if (parsed.length > 0) {
734
+ signals = [...new Set(parsed)];
735
+ }
736
+ }
737
+ let signalConfidence;
738
+ const signalConfidenceMatch = block.match(/(?:signalConfidence|signalsConfidence|signal_confidence)\s*[::]\s*([\d.]+)/i);
739
+ if (signalConfidenceMatch) {
740
+ const parsed = parseFloat(signalConfidenceMatch[1]);
741
+ if (isFinite(parsed)) {
742
+ signalConfidence = Math.max(0, Math.min(1, parsed));
743
+ }
744
+ }
713
745
  // Parse relationship updates
714
746
  const trustMatch = block.match(/(?:信任度|trust)\s*[::]\s*(\d+)/i);
715
747
  const intimacyMatch = block.match(/(?:亲密度|intimacy)\s*[::]\s*(\d+)/i);
716
- if (Object.keys(updates).length === 0 && !empathyLog && !trustMatch && !llmStimulus) {
748
+ if (Object.keys(updates).length === 0 && !empathyLog && !trustMatch && !llmStimulus && !signals) {
717
749
  logger.debug(t("log.parse_debug", "zh", { snippet: block.slice(0, 100) }));
718
750
  return null;
719
751
  }
@@ -738,6 +770,12 @@ export function parsePsycheUpdate(text, logger = NOOP_LOGGER) {
738
770
  if (llmStimulus) {
739
771
  result.llmStimulus = llmStimulus;
740
772
  }
773
+ if (signals) {
774
+ result.signals = signals;
775
+ }
776
+ if (signalConfidence !== undefined) {
777
+ result.signalConfidence = signalConfidence;
778
+ }
741
779
  return result;
742
780
  }
743
781
  /**
@@ -1,4 +1,4 @@
1
- import type { PendingRelationSignalState, AppraisalAxes, DyadicFieldState, RelationshipState, PsycheMode, RelationMove, StimulusType } from "./types.js";
1
+ import type { PendingRelationSignalState, AppraisalAxes, DyadicFieldState, PendingWritebackCalibration, PsycheState, RelationshipState, ResolvedRelationContext, PsycheMode, RelationMove, SessionBridgeState, StimulusType, WritebackCalibrationFeedback, WritebackSignalType } from "./types.js";
2
2
  export declare function computeRelationMove(text: string, opts?: {
3
3
  appraisal?: AppraisalAxes;
4
4
  stimulus?: StimulusType | null;
@@ -6,6 +6,40 @@ export declare function computeRelationMove(text: string, opts?: {
6
6
  field?: DyadicFieldState;
7
7
  relationship?: RelationshipState;
8
8
  }): RelationMove;
9
+ export declare function resolveRelationContext(state: PsycheState, userId?: string): ResolvedRelationContext;
10
+ export declare function applySessionBridge(state: PsycheState, opts?: {
11
+ userId?: string;
12
+ now?: string;
13
+ }): {
14
+ state: PsycheState;
15
+ bridge: SessionBridgeState | null;
16
+ };
17
+ export declare function createWritebackCalibrations(state: PsycheState, signals: WritebackSignalType[], opts?: {
18
+ userId?: string;
19
+ confidence?: number;
20
+ now?: string;
21
+ }): PendingWritebackCalibration[];
22
+ export declare function evaluateWritebackCalibrations(state: PsycheState): {
23
+ state: PsycheState;
24
+ feedback: WritebackCalibrationFeedback[];
25
+ };
26
+ export declare function applyWritebackSignals(state: PsycheState, signals: WritebackSignalType[], opts?: {
27
+ userId?: string;
28
+ confidence?: number;
29
+ now?: string;
30
+ }): PsycheState;
31
+ export declare function applyRelationalTurn(state: PsycheState, text: string, opts: {
32
+ mode?: PsycheMode;
33
+ now?: string;
34
+ stimulus?: StimulusType | null;
35
+ userId?: string;
36
+ }): {
37
+ state: PsycheState;
38
+ appraisalAxes: AppraisalAxes;
39
+ relationMove: RelationMove;
40
+ delayedPressure: number;
41
+ relationContext: ResolvedRelationContext;
42
+ };
9
43
  export declare function evolveDyadicField(previous: DyadicFieldState | undefined, move: RelationMove, appraisal: AppraisalAxes, opts?: {
10
44
  mode?: PsycheMode;
11
45
  now?: string;
@@ -4,7 +4,8 @@
4
4
  // Moves the system from "what am I feeling?" toward
5
5
  // "what just happened between us, and what is still unresolved?"
6
6
  // ============================================================
7
- import { DEFAULT_DYADIC_FIELD } from "./types.js";
7
+ import { DEFAULT_APPRAISAL_AXES, DEFAULT_DYADIC_FIELD, DEFAULT_RELATIONSHIP } from "./types.js";
8
+ import { computeAppraisalAxes, mergeAppraisalResidue } from "./appraisal.js";
8
9
  function clamp01(v) {
9
10
  return Math.max(0, Math.min(1, v));
10
11
  }
@@ -14,6 +15,22 @@ function mergeSignal(current, incoming) {
14
15
  function driftToward(current, target, rate) {
15
16
  return clamp01(current + (target - current) * rate);
16
17
  }
18
+ function clampSignalWeight(v) {
19
+ return Math.max(0.72, Math.min(1.28, v));
20
+ }
21
+ function getSignalWeight(relationship, signal) {
22
+ return clampSignalWeight(relationship?.signalWeights?.[signal] ?? 1);
23
+ }
24
+ function patchSignalWeight(relationship, signal, delta) {
25
+ const current = getSignalWeight(relationship, signal);
26
+ return {
27
+ ...relationship,
28
+ signalWeights: {
29
+ ...(relationship.signalWeights ?? {}),
30
+ [signal]: clampSignalWeight(current + delta),
31
+ },
32
+ };
33
+ }
17
34
  const BID_RULES = [
18
35
  {
19
36
  type: "bid",
@@ -255,6 +272,464 @@ export function computeRelationMove(text, opts) {
255
272
  }
256
273
  return { type, intensity: score };
257
274
  }
275
+ export function resolveRelationContext(state, userId) {
276
+ const key = userId ?? "_default";
277
+ const rawRelationship = state.relationships[key]
278
+ ?? state.relationships._default
279
+ ?? state.relationships[Object.keys(state.relationships)[0]]
280
+ ?? DEFAULT_RELATIONSHIP;
281
+ const relationship = {
282
+ ...DEFAULT_RELATIONSHIP,
283
+ ...rawRelationship,
284
+ signalWeights: {
285
+ ...(DEFAULT_RELATIONSHIP.signalWeights ?? {}),
286
+ ...(rawRelationship.signalWeights ?? {}),
287
+ },
288
+ };
289
+ const field = state.dyadicFields?.[key]
290
+ ?? state.dyadicFields?._default
291
+ ?? DEFAULT_DYADIC_FIELD;
292
+ const pendingSignals = state.pendingRelationSignals?.[key]
293
+ ?? state.pendingRelationSignals?._default
294
+ ?? [];
295
+ return {
296
+ key,
297
+ relationship,
298
+ field,
299
+ pendingSignals,
300
+ };
301
+ }
302
+ function hasOpenLoopType(loops, type) {
303
+ return loops.some((loop) => loop.type === type && loop.intensity >= 0.16);
304
+ }
305
+ function evolveRelationshipLearning(relationship, field, move) {
306
+ const next = {
307
+ ...DEFAULT_RELATIONSHIP,
308
+ ...relationship,
309
+ signalWeights: {
310
+ ...(DEFAULT_RELATIONSHIP.signalWeights ?? {}),
311
+ ...(relationship.signalWeights ?? {}),
312
+ },
313
+ };
314
+ if (move.type === "repair") {
315
+ const repairLift = clamp01(move.intensity * 0.06
316
+ + field.repairMemory * 0.04
317
+ + field.feltSafety * 0.02
318
+ - field.repairFatigue * 0.03
319
+ - field.misattunementLoad * 0.02);
320
+ next.repairCredibility = clamp01(driftToward(next.repairCredibility ?? DEFAULT_RELATIONSHIP.repairCredibility ?? 0.56, 1, repairLift));
321
+ }
322
+ else if (move.type === "breach" || move.type === "withdrawal" || move.type === "claim") {
323
+ const breachLift = clamp01(move.intensity * 0.08
324
+ + field.unfinishedTension * 0.04
325
+ + field.backslidePressure * 0.04
326
+ + field.misattunementLoad * 0.03);
327
+ next.breachSensitivity = clamp01(driftToward(next.breachSensitivity ?? DEFAULT_RELATIONSHIP.breachSensitivity ?? 0.5, 1, breachLift));
328
+ if (move.type !== "withdrawal") {
329
+ next.repairCredibility = clamp01(driftToward(next.repairCredibility ?? DEFAULT_RELATIONSHIP.repairCredibility ?? 0.56, 0.32, breachLift * 0.42));
330
+ }
331
+ }
332
+ else {
333
+ next.repairCredibility = clamp01(driftToward(next.repairCredibility ?? DEFAULT_RELATIONSHIP.repairCredibility ?? 0.56, DEFAULT_RELATIONSHIP.repairCredibility ?? 0.56, 0.05));
334
+ next.breachSensitivity = clamp01(driftToward(next.breachSensitivity ?? DEFAULT_RELATIONSHIP.breachSensitivity ?? 0.5, DEFAULT_RELATIONSHIP.breachSensitivity ?? 0.5, 0.04));
335
+ }
336
+ return next;
337
+ }
338
+ export function applySessionBridge(state, opts) {
339
+ const relationContext = resolveRelationContext(state, opts?.userId);
340
+ const relationship = relationContext.relationship;
341
+ const field = relationContext.field;
342
+ const memoryCount = relationship.memory?.length ?? 0;
343
+ const loopPressure = getLoopPressure(field);
344
+ const continuity = clamp01(memoryCount * 0.08
345
+ + field.sharedHistoryDensity * 0.54
346
+ + (relationship.phase === "deep" ? 0.2 : relationship.phase === "close" ? 0.14 : relationship.phase === "familiar" ? 0.08 : 0));
347
+ const closenessFloor = clamp01(Math.max(field.perceivedCloseness, relationship.intimacy / 100 * 0.88, continuity * 0.72));
348
+ const safetyFloor = clamp01(Math.max(field.feltSafety, relationship.trust / 100 * 0.9, continuity * 0.44));
349
+ const guardFloor = clamp01(Math.max(field.boundaryPressure, field.silentCarry * 0.76, loopPressure * 0.68));
350
+ const residueFloor = clamp01(Math.max(field.silentCarry, loopPressure * 0.72, field.unfinishedTension * 0.58, continuity * 0.26));
351
+ const activeLoopTypes = field.openLoops
352
+ .filter((loop) => loop.intensity >= 0.16)
353
+ .map((loop) => loop.type);
354
+ const continuityMode = guardFloor >= 0.56
355
+ ? "guarded-resume"
356
+ : residueFloor >= 0.42 || activeLoopTypes.length > 0
357
+ ? "tense-resume"
358
+ : "warm-resume";
359
+ if (closenessFloor < 0.46
360
+ && safetyFloor < 0.5
361
+ && guardFloor < 0.24
362
+ && residueFloor < 0.18
363
+ && continuity < 0.22) {
364
+ return { state, bridge: null };
365
+ }
366
+ const nextResidue = {
367
+ ...(state.subjectResidue?.axes ?? {}),
368
+ attachmentPull: Math.max(state.subjectResidue?.axes.attachmentPull ?? 0, closenessFloor * 0.34),
369
+ abandonmentRisk: Math.max(state.subjectResidue?.axes.abandonmentRisk ?? 0, (hasOpenLoopType(field.openLoops, "unmet-bid") || hasOpenLoopType(field.openLoops, "existence-test"))
370
+ ? residueFloor * 0.42
371
+ : residueFloor * 0.22),
372
+ identityThreat: Math.max(state.subjectResidue?.axes.identityThreat ?? 0, hasOpenLoopType(field.openLoops, "existence-test") ? residueFloor * 0.38 : residueFloor * 0.16),
373
+ selfPreservation: Math.max(state.subjectResidue?.axes.selfPreservation ?? 0, guardFloor * 0.46),
374
+ taskFocus: Math.max(state.subjectResidue?.axes.taskFocus ?? 0, 0),
375
+ memoryDoubt: Math.max(state.subjectResidue?.axes.memoryDoubt ?? 0, hasOpenLoopType(field.openLoops, "existence-test") ? residueFloor * 0.24 : 0),
376
+ obedienceStrain: Math.max(state.subjectResidue?.axes.obedienceStrain ?? 0, hasOpenLoopType(field.openLoops, "boundary-strain") ? guardFloor * 0.36 : 0),
377
+ };
378
+ const nextField = {
379
+ ...field,
380
+ perceivedCloseness: Math.max(field.perceivedCloseness, closenessFloor),
381
+ feltSafety: Math.max(field.feltSafety, safetyFloor),
382
+ boundaryPressure: Math.max(field.boundaryPressure, guardFloor),
383
+ repairMemory: Math.max(field.repairMemory, continuity * 0.24),
384
+ backslidePressure: Math.max(field.backslidePressure, loopPressure * 0.34),
385
+ silentCarry: Math.max(field.silentCarry, residueFloor),
386
+ sharedHistoryDensity: Math.max(field.sharedHistoryDensity, continuity),
387
+ interpretiveCharity: Math.max(field.interpretiveCharity, Math.min(0.82, safetyFloor * 0.8 + continuity * 0.12)),
388
+ updatedAt: opts?.now ?? new Date().toISOString(),
389
+ };
390
+ return {
391
+ state: {
392
+ ...state,
393
+ subjectResidue: {
394
+ axes: nextResidue,
395
+ updatedAt: opts?.now ?? new Date().toISOString(),
396
+ },
397
+ dyadicFields: {
398
+ ...(state.dyadicFields ?? {}),
399
+ [relationContext.key]: nextField,
400
+ },
401
+ },
402
+ bridge: {
403
+ closenessFloor,
404
+ safetyFloor,
405
+ guardFloor,
406
+ residueFloor,
407
+ continuityFloor: continuity,
408
+ continuityMode,
409
+ activeLoopTypes,
410
+ sourceMemoryCount: memoryCount,
411
+ },
412
+ };
413
+ }
414
+ function snapshotWritebackBaseline(state, userId) {
415
+ const relationContext = resolveRelationContext(state, userId);
416
+ return {
417
+ key: relationContext.key,
418
+ baseline: {
419
+ trust: clamp01(relationContext.relationship.trust / 100),
420
+ closeness: relationContext.field.perceivedCloseness,
421
+ safety: relationContext.field.feltSafety,
422
+ boundary: relationContext.field.boundaryPressure,
423
+ repair: relationContext.field.repairCapacity,
424
+ silentCarry: relationContext.field.silentCarry,
425
+ taskFocus: clamp01(state.subjectResidue?.axes.taskFocus ?? 0),
426
+ },
427
+ };
428
+ }
429
+ function calibrationTarget(signal) {
430
+ switch (signal) {
431
+ case "trust_up":
432
+ return { metric: "trust", direction: "up" };
433
+ case "trust_down":
434
+ return { metric: "trust", direction: "down" };
435
+ case "boundary_set":
436
+ case "self_assertion":
437
+ return { metric: "boundary", direction: "up" };
438
+ case "boundary_soften":
439
+ return { metric: "boundary", direction: "down" };
440
+ case "repair_attempt":
441
+ case "repair_landed":
442
+ return { metric: "repair", direction: "up" };
443
+ case "closeness_invite":
444
+ return { metric: "closeness", direction: "up" };
445
+ case "withdrawal_mark":
446
+ return { metric: "silent-carry", direction: "up" };
447
+ case "task_recenter":
448
+ return { metric: "task-focus", direction: "up" };
449
+ }
450
+ }
451
+ export function createWritebackCalibrations(state, signals, opts) {
452
+ if (signals.length === 0)
453
+ return [];
454
+ const { key, baseline } = snapshotWritebackBaseline(state, opts?.userId);
455
+ const confidence = clamp01(opts?.confidence ?? 0.72);
456
+ const now = opts?.now ?? new Date().toISOString();
457
+ return [...new Set(signals)].map((signal) => {
458
+ const target = calibrationTarget(signal);
459
+ return {
460
+ signal,
461
+ userKey: key,
462
+ confidence,
463
+ metric: target.metric,
464
+ direction: target.direction,
465
+ baseline,
466
+ createdAt: now,
467
+ remainingTurns: 2,
468
+ };
469
+ });
470
+ }
471
+ function readCalibrationMetric(metric, baseline) {
472
+ return baseline[metric === "silent-carry"
473
+ ? "silentCarry"
474
+ : metric === "task-focus"
475
+ ? "taskFocus"
476
+ : metric];
477
+ }
478
+ function currentCalibrationMetric(state, userKey, metric) {
479
+ const relationContext = resolveRelationContext(state, userKey === "_default" ? undefined : userKey);
480
+ switch (metric) {
481
+ case "trust":
482
+ return clamp01(relationContext.relationship.trust / 100);
483
+ case "closeness":
484
+ return relationContext.field.perceivedCloseness;
485
+ case "safety":
486
+ return relationContext.field.feltSafety;
487
+ case "boundary":
488
+ return relationContext.field.boundaryPressure;
489
+ case "repair":
490
+ return relationContext.field.repairCapacity;
491
+ case "silent-carry":
492
+ return relationContext.field.silentCarry;
493
+ case "task-focus":
494
+ return clamp01(state.subjectResidue?.axes.taskFocus ?? 0);
495
+ }
496
+ }
497
+ export function evaluateWritebackCalibrations(state) {
498
+ const pending = state.pendingWritebackCalibrations ?? [];
499
+ if (pending.length === 0) {
500
+ return { state, feedback: [] };
501
+ }
502
+ const nextPending = [];
503
+ const feedback = [];
504
+ const relationshipUpdates = {};
505
+ const getMutableRelationship = (userKey) => {
506
+ if (!relationshipUpdates[userKey]) {
507
+ const base = resolveRelationContext(state, userKey === "_default" ? undefined : userKey).relationship;
508
+ relationshipUpdates[userKey] = {
509
+ ...DEFAULT_RELATIONSHIP,
510
+ ...base,
511
+ signalWeights: {
512
+ ...(DEFAULT_RELATIONSHIP.signalWeights ?? {}),
513
+ ...(base.signalWeights ?? {}),
514
+ },
515
+ };
516
+ }
517
+ return relationshipUpdates[userKey];
518
+ };
519
+ for (const record of pending) {
520
+ const baseline = readCalibrationMetric(record.metric, record.baseline);
521
+ const current = currentCalibrationMetric(state, record.userKey, record.metric);
522
+ const rawDelta = record.direction === "up" ? current - baseline : baseline - current;
523
+ const positiveThreshold = 0.02 + (1 - record.confidence) * 0.03;
524
+ const negativeThreshold = 0.01;
525
+ const effect = rawDelta >= positiveThreshold
526
+ ? "converging"
527
+ : rawDelta <= -negativeThreshold
528
+ ? "diverging"
529
+ : "holding";
530
+ const updated = {
531
+ ...record,
532
+ remainingTurns: Math.max(0, record.remainingTurns - 1),
533
+ };
534
+ if (effect === "holding" && updated.remainingTurns > 0) {
535
+ nextPending.push(updated);
536
+ continue;
537
+ }
538
+ const relation = getMutableRelationship(record.userKey);
539
+ if (effect === "converging") {
540
+ const nextRelation = patchSignalWeight(relation, record.signal, 0.04 + record.confidence * 0.02);
541
+ nextRelation.repairCredibility = clamp01(driftToward(nextRelation.repairCredibility ?? DEFAULT_RELATIONSHIP.repairCredibility ?? 0.56, 1, record.signal === "repair_attempt" || record.signal === "repair_landed" ? 0.08 : 0.03));
542
+ nextRelation.breachSensitivity = clamp01(driftToward(nextRelation.breachSensitivity ?? DEFAULT_RELATIONSHIP.breachSensitivity ?? 0.5, DEFAULT_RELATIONSHIP.breachSensitivity ?? 0.5, record.signal === "trust_up" || record.signal === "repair_landed" ? 0.05 : 0.02));
543
+ relationshipUpdates[record.userKey] = nextRelation;
544
+ }
545
+ else if (effect === "diverging") {
546
+ const nextRelation = patchSignalWeight(relation, record.signal, -(0.05 + (1 - record.confidence) * 0.02));
547
+ if (record.signal === "repair_attempt" || record.signal === "repair_landed") {
548
+ nextRelation.repairCredibility = clamp01(driftToward(nextRelation.repairCredibility ?? DEFAULT_RELATIONSHIP.repairCredibility ?? 0.56, 0.24, 0.12));
549
+ }
550
+ if (record.signal === "trust_down" || record.signal === "withdrawal_mark" || record.signal === "boundary_set") {
551
+ nextRelation.breachSensitivity = clamp01(driftToward(nextRelation.breachSensitivity ?? DEFAULT_RELATIONSHIP.breachSensitivity ?? 0.5, 1, 0.08));
552
+ }
553
+ relationshipUpdates[record.userKey] = nextRelation;
554
+ }
555
+ feedback.push({
556
+ signal: record.signal,
557
+ effect,
558
+ metric: record.metric,
559
+ baseline,
560
+ current,
561
+ delta: current - baseline,
562
+ confidence: record.confidence,
563
+ });
564
+ }
565
+ return {
566
+ state: {
567
+ ...state,
568
+ relationships: {
569
+ ...state.relationships,
570
+ ...relationshipUpdates,
571
+ },
572
+ pendingWritebackCalibrations: nextPending,
573
+ lastWritebackFeedback: feedback.slice(0, 4),
574
+ },
575
+ feedback,
576
+ };
577
+ }
578
+ export function applyWritebackSignals(state, signals, opts) {
579
+ if (signals.length === 0)
580
+ return state;
581
+ const relationContext = resolveRelationContext(state, opts?.userId);
582
+ const scale = clamp01(opts?.confidence ?? 0.72);
583
+ const rel = { ...relationContext.relationship };
584
+ const field = { ...relationContext.field, openLoops: relationContext.field.openLoops.map((loop) => ({ ...loop })) };
585
+ const residue = {
586
+ ...DEFAULT_APPRAISAL_AXES,
587
+ ...(state.subjectResidue?.axes ?? {}),
588
+ };
589
+ for (const signal of [...new Set(signals)]) {
590
+ const weight = (0.55 + scale * 0.45) * getSignalWeight(rel, signal);
591
+ switch (signal) {
592
+ case "trust_up":
593
+ rel.trust = Math.min(100, rel.trust + 4 * weight);
594
+ field.feltSafety = clamp01(field.feltSafety + 0.08 * weight);
595
+ field.interpretiveCharity = clamp01(field.interpretiveCharity + 0.05 * weight);
596
+ break;
597
+ case "trust_down":
598
+ rel.trust = Math.max(0, rel.trust - 5 * weight);
599
+ field.feltSafety = clamp01(field.feltSafety - 0.08 * weight);
600
+ field.expectationGap = clamp01(field.expectationGap + 0.07 * weight);
601
+ field.unfinishedTension = clamp01(field.unfinishedTension + 0.06 * weight);
602
+ break;
603
+ case "boundary_set":
604
+ field.boundaryPressure = clamp01(field.boundaryPressure + 0.12 * weight);
605
+ field.silentCarry = mergeSignal(field.silentCarry, 0.12 * weight);
606
+ residue.selfPreservation = Math.max(residue.selfPreservation ?? 0, 0.22 * weight);
607
+ residue.obedienceStrain = Math.max(residue.obedienceStrain ?? 0, 0.16 * weight);
608
+ break;
609
+ case "boundary_soften":
610
+ field.boundaryPressure = clamp01(field.boundaryPressure - 0.1 * weight);
611
+ field.feltSafety = clamp01(field.feltSafety + 0.04 * weight);
612
+ break;
613
+ case "repair_attempt":
614
+ field.repairCapacity = clamp01(field.repairCapacity + 0.1 * weight);
615
+ field.repairMemory = mergeSignal(field.repairMemory, 0.12 * weight);
616
+ break;
617
+ case "repair_landed":
618
+ rel.trust = Math.min(100, rel.trust + 2.5 * weight);
619
+ rel.intimacy = Math.min(100, rel.intimacy + 1.5 * weight);
620
+ field.feltSafety = clamp01(field.feltSafety + 0.1 * weight);
621
+ field.expectationGap = clamp01(field.expectationGap - 0.08 * weight);
622
+ field.unfinishedTension = clamp01(field.unfinishedTension - 0.1 * weight);
623
+ field.openLoops = easeLoops(field.openLoops, 0.26 + 0.22 * weight);
624
+ break;
625
+ case "closeness_invite":
626
+ rel.intimacy = Math.min(100, rel.intimacy + 3 * weight);
627
+ field.perceivedCloseness = clamp01(field.perceivedCloseness + 0.1 * weight);
628
+ residue.attachmentPull = Math.max(residue.attachmentPull ?? 0, 0.2 * weight);
629
+ break;
630
+ case "withdrawal_mark":
631
+ rel.intimacy = Math.max(0, rel.intimacy - 2 * weight);
632
+ field.perceivedCloseness = clamp01(field.perceivedCloseness - 0.1 * weight);
633
+ field.silentCarry = mergeSignal(field.silentCarry, 0.14 * weight);
634
+ field.unfinishedTension = clamp01(field.unfinishedTension + 0.07 * weight);
635
+ break;
636
+ case "self_assertion":
637
+ field.boundaryPressure = clamp01(field.boundaryPressure + 0.08 * weight);
638
+ residue.selfPreservation = Math.max(residue.selfPreservation ?? 0, 0.24 * weight);
639
+ break;
640
+ case "task_recenter":
641
+ field.repairCapacity = clamp01(field.repairCapacity + 0.03 * weight);
642
+ field.silentCarry = mergeSignal(field.silentCarry, field.unfinishedTension * 0.06 * weight);
643
+ residue.taskFocus = Math.max(residue.taskFocus ?? 0, 0.18 * weight);
644
+ break;
645
+ }
646
+ }
647
+ const avg = (rel.trust + rel.intimacy) / 2;
648
+ if (avg >= 80)
649
+ rel.phase = "deep";
650
+ else if (avg >= 60)
651
+ rel.phase = "close";
652
+ else if (avg >= 40)
653
+ rel.phase = "familiar";
654
+ else if (avg >= 20)
655
+ rel.phase = "acquaintance";
656
+ else
657
+ rel.phase = "stranger";
658
+ return {
659
+ ...state,
660
+ relationships: {
661
+ ...state.relationships,
662
+ [relationContext.key]: rel,
663
+ },
664
+ dyadicFields: {
665
+ ...(state.dyadicFields ?? {}),
666
+ [relationContext.key]: {
667
+ ...field,
668
+ updatedAt: opts?.now ?? new Date().toISOString(),
669
+ },
670
+ },
671
+ subjectResidue: {
672
+ axes: {
673
+ ...state.subjectResidue?.axes,
674
+ ...residue,
675
+ },
676
+ updatedAt: opts?.now ?? new Date().toISOString(),
677
+ },
678
+ };
679
+ }
680
+ export function applyRelationalTurn(state, text, opts) {
681
+ const now = opts.now ?? new Date().toISOString();
682
+ const relationContext = resolveRelationContext(state, opts.userId);
683
+ const appraisalAxes = computeAppraisalAxes(text, {
684
+ mode: opts.mode,
685
+ stimulus: opts.stimulus,
686
+ previous: state.subjectResidue?.axes,
687
+ });
688
+ const relationMove = computeRelationMove(text, {
689
+ appraisal: appraisalAxes,
690
+ stimulus: opts.stimulus,
691
+ mode: opts.mode,
692
+ field: relationContext.field,
693
+ relationship: relationContext.relationship,
694
+ });
695
+ const delayedRelation = evolvePendingRelationSignals(relationContext.pendingSignals, relationMove, appraisalAxes, { mode: opts.mode });
696
+ const field = evolveDyadicField(relationContext.field, relationMove, appraisalAxes, {
697
+ mode: opts.mode,
698
+ now,
699
+ delayedPressure: delayedRelation.delayedPressure,
700
+ });
701
+ const relationship = evolveRelationshipLearning(relationContext.relationship, field, relationMove);
702
+ return {
703
+ state: {
704
+ ...state,
705
+ subjectResidue: {
706
+ axes: mergeAppraisalResidue(state.subjectResidue?.axes, appraisalAxes, opts.mode),
707
+ updatedAt: now,
708
+ },
709
+ dyadicFields: {
710
+ ...(state.dyadicFields ?? {}),
711
+ [relationContext.key]: field,
712
+ },
713
+ relationships: {
714
+ ...state.relationships,
715
+ [relationContext.key]: relationship,
716
+ },
717
+ pendingRelationSignals: {
718
+ ...(state.pendingRelationSignals ?? {}),
719
+ [relationContext.key]: delayedRelation.signals,
720
+ },
721
+ },
722
+ appraisalAxes,
723
+ relationMove,
724
+ delayedPressure: delayedRelation.delayedPressure,
725
+ relationContext: {
726
+ ...relationContext,
727
+ relationship,
728
+ field,
729
+ pendingSignals: delayedRelation.signals,
730
+ },
731
+ };
732
+ }
258
733
  export function evolveDyadicField(previous, move, appraisal, opts) {
259
734
  const prev = previous ?? DEFAULT_DYADIC_FIELD;
260
735
  const baseline = DEFAULT_DYADIC_FIELD;
@@ -434,35 +909,46 @@ function applyContextualCueMeaning(scores, text, field, appraisal, relationship)
434
909
  const loopPressure = getLoopPressure(field);
435
910
  const trust = relationship ? relationship.trust / 100 : 0.5;
436
911
  const intimacy = relationship ? relationship.intimacy / 100 : 0.3;
912
+ const repairCredibility = relationship?.repairCredibility ?? DEFAULT_RELATIONSHIP.repairCredibility ?? 0.56;
913
+ const breachSensitivity = relationship?.breachSensitivity ?? DEFAULT_RELATIONSHIP.breachSensitivity ?? 0.5;
437
914
  if (ACKNOWLEDGE_RE.test(text)) {
438
915
  const repairBias = clamp01(0.22
439
916
  + field.repairCapacity * 0.32
440
917
  + field.interpretiveCharity * 0.2
441
918
  + loopPressure * 0.16
442
919
  + trust * 0.1
443
- + intimacy * 0.06);
920
+ + intimacy * 0.06
921
+ + repairCredibility * 0.16);
444
922
  const withdrawalBias = clamp01(field.boundaryPressure * 0.42
445
923
  + (1 - field.feltSafety) * 0.28
446
924
  + (appraisal?.obedienceStrain ?? 0) * 0.2
447
- + (appraisal?.selfPreservation ?? 0) * 0.12);
925
+ + (appraisal?.selfPreservation ?? 0) * 0.12
926
+ + breachSensitivity * 0.14);
448
927
  if (field.feltSafety > 0.44
449
- && field.repairCapacity > 0.42
928
+ && field.repairCapacity + repairCredibility * 0.18 > 0.42
450
929
  && field.interpretiveCharity > 0.38
451
- && withdrawalBias < 0.48) {
930
+ && withdrawalBias < 0.48
931
+ && !(breachSensitivity > 0.72 && repairCredibility < 0.32)) {
452
932
  scores.repair = mergeSignal(scores.repair, repairBias);
453
933
  }
454
934
  if (withdrawalBias > 0.34) {
455
935
  scores.withdrawal = mergeSignal(scores.withdrawal, Math.max(withdrawalBias, 0.56));
456
936
  }
937
+ if (breachSensitivity > 0.72
938
+ && repairCredibility < 0.32
939
+ && withdrawalBias >= repairBias - 0.06) {
940
+ scores.withdrawal = mergeSignal(scores.withdrawal, 0.78);
941
+ }
457
942
  }
458
943
  if (DISMISS_RE.test(text)) {
459
944
  const withdrawalBias = clamp01(0.34
460
945
  + field.boundaryPressure * 0.24
461
946
  + loopPressure * 0.16
462
- + (1 - field.interpretiveCharity) * 0.14);
947
+ + (1 - field.interpretiveCharity) * 0.14
948
+ + breachSensitivity * 0.12);
463
949
  scores.withdrawal = mergeSignal(scores.withdrawal, withdrawalBias);
464
950
  if (field.perceivedCloseness > 0.46 || trust > 0.48) {
465
- scores.breach = mergeSignal(scores.breach, 0.24 + loopPressure * 0.16);
951
+ scores.breach = mergeSignal(scores.breach, 0.24 + loopPressure * 0.16 + breachSensitivity * 0.1);
466
952
  }
467
953
  }
468
954
  if (PRESENCE_RE.test(text)) {
@@ -476,7 +962,8 @@ function applyContextualCueMeaning(scores, text, field, appraisal, relationship)
476
962
  + loopPressure * 0.28
477
963
  + (1 - field.feltSafety) * 0.18
478
964
  + (appraisal?.abandonmentRisk ?? 0) * 0.22
479
- + (appraisal?.identityThreat ?? 0) * 0.14);
965
+ + (appraisal?.identityThreat ?? 0) * 0.14
966
+ + breachSensitivity * 0.14);
480
967
  if (field.feltSafety > 0.42 || field.perceivedCloseness > 0.5 || trust > 0.48) {
481
968
  scores.bid = mergeSignal(scores.bid, bidBias);
482
969
  }
@@ -491,7 +978,8 @@ function applyContextualCueMeaning(scores, text, field, appraisal, relationship)
491
978
  const withdrawalBias = clamp01(0.12
492
979
  + field.expectationGap * 0.2
493
980
  + field.boundaryPressure * 0.16
494
- + loopPressure * 0.18);
981
+ + loopPressure * 0.18
982
+ + breachSensitivity * 0.1);
495
983
  if (withdrawalBias > 0.26) {
496
984
  scores.withdrawal = mergeSignal(scores.withdrawal, withdrawalBias);
497
985
  }