dsh-biocore 0.2.0 → 0.3.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.
Files changed (245) hide show
  1. package/README.md +11 -0
  2. package/lib/core-worker.js +268 -0
  3. package/lib/index.js +160 -70
  4. package/lib/modules/autonomy.js +125 -0
  5. package/lib/modules/cache-stats.js +48 -0
  6. package/lib/modules/core-turn.js +24 -6
  7. package/lib/modules/expression-inject.js +0 -10
  8. package/lib/modules/expression-refresh.js +0 -9
  9. package/lib/modules/memory-capture.js +2 -1
  10. package/lib/modules/memory-inject.js +50 -5
  11. package/lib/modules/memory-tools.js +7 -4
  12. package/lib/modules/python-env.js +46 -0
  13. package/lib/modules/safety-gate.js +0 -3
  14. package/lib/modules/series.js +54 -0
  15. package/lib/modules/switch.js +16 -0
  16. package/lib/py-bridge.js +81 -15
  17. package/package.json +1 -1
  18. package/python/biocore_memory/core/config.py +138 -127
  19. package/python/biocore_memory/engine.py +2653 -2476
  20. package/python/biocore_memory/recall/embedding.py +129 -0
  21. package/python/biocore_memory/recall/feedback.py +202 -0
  22. package/python/biocore_model/preference_drift.py +255 -0
  23. package/python/biocore_model/tests/__init__.py +1 -0
  24. package/python/biocore_model/tests/core_mode_ui_check.mjs +63 -0
  25. package/python/biocore_model/tests/core_status_check.mjs +29 -0
  26. package/python/biocore_model/tests/dom_safe_check.mjs +22 -0
  27. package/python/biocore_model/tests/leak_samples_run.py +18 -0
  28. package/python/biocore_model/tests/orb_ring_check.mjs +20 -0
  29. package/python/biocore_model/tests/panel_html_check.mjs +63 -0
  30. package/python/biocore_model/tests/persona_flow_check.mjs +17 -0
  31. package/python/biocore_model/tests/render_guard_check.mjs +19 -0
  32. package/python/biocore_model/tests/switch_groups_check.mjs +94 -0
  33. package/python/biocore_model/tests/tabs_state_check.mjs +24 -0
  34. package/python/biocore_model/tests/test_affective_labor.py +102 -0
  35. package/python/biocore_model/tests/test_appraisal.py +137 -0
  36. package/python/biocore_model/tests/test_appraisal_action.py +55 -0
  37. package/python/biocore_model/tests/test_arch_convergence.py +35 -0
  38. package/python/biocore_model/tests/test_attachment_behavior.py +95 -0
  39. package/python/biocore_model/tests/test_attachment_regulation.py +63 -0
  40. package/python/biocore_model/tests/test_attachment_state.py +29 -0
  41. package/python/biocore_model/tests/test_attribution.py +56 -0
  42. package/python/biocore_model/tests/test_attributor.py +108 -0
  43. package/python/biocore_model/tests/test_autonomic.py +69 -0
  44. package/python/biocore_model/tests/test_autonomy_accumulator.py +84 -0
  45. package/python/biocore_model/tests/test_autonomy_affect.py +62 -0
  46. package/python/biocore_model/tests/test_autonomy_blame.py +44 -0
  47. package/python/biocore_model/tests/test_autonomy_caps.py +29 -0
  48. package/python/biocore_model/tests/test_autonomy_check.py +40 -0
  49. package/python/biocore_model/tests/test_autonomy_clingy.py +41 -0
  50. package/python/biocore_model/tests/test_autonomy_cooldown_selfcontact.py +138 -0
  51. package/python/biocore_model/tests/test_autonomy_frustration.py +42 -0
  52. package/python/biocore_model/tests/test_autonomy_hourly_limit.py +46 -0
  53. package/python/biocore_model/tests/test_autonomy_loop.py +64 -0
  54. package/python/biocore_model/tests/test_autonomy_mode.py +37 -0
  55. package/python/biocore_model/tests/test_autonomy_model.py +60 -0
  56. package/python/biocore_model/tests/test_autonomy_model_v2.py +68 -0
  57. package/python/biocore_model/tests/test_autonomy_module_text.py +104 -0
  58. package/python/biocore_model/tests/test_autonomy_params.py +163 -0
  59. package/python/biocore_model/tests/test_autonomy_reason.py +34 -0
  60. package/python/biocore_model/tests/test_autonomy_self.py +153 -0
  61. package/python/biocore_model/tests/test_autonomy_send_gate.py +38 -0
  62. package/python/biocore_model/tests/test_autonomy_silence.py +37 -0
  63. package/python/biocore_model/tests/test_autonomy_status.py +35 -0
  64. package/python/biocore_model/tests/test_autonomy_test_trigger.py +24 -0
  65. package/python/biocore_model/tests/test_autonomy_tick.py +148 -0
  66. package/python/biocore_model/tests/test_autonomy_v4.py +64 -0
  67. package/python/biocore_model/tests/test_autonomy_v5.py +43 -0
  68. package/python/biocore_model/tests/test_autonomy_v7.py +48 -0
  69. package/python/biocore_model/tests/test_autonomy_v_theta.py +85 -0
  70. package/python/biocore_model/tests/test_batch1_wiring.py +231 -0
  71. package/python/biocore_model/tests/test_batch2_wiring.py +231 -0
  72. package/python/biocore_model/tests/test_batch3_wiring.py +223 -0
  73. package/python/biocore_model/tests/test_bio_tick.py +202 -0
  74. package/python/biocore_model/tests/test_boundary.py +183 -0
  75. package/python/biocore_model/tests/test_boundary_gate.py +295 -0
  76. package/python/biocore_model/tests/test_brain_split.py +99 -0
  77. package/python/biocore_model/tests/test_co_recall.py +350 -0
  78. package/python/biocore_model/tests/test_commitment_extractor.py +197 -0
  79. package/python/biocore_model/tests/test_compassion_fatigue.py +84 -0
  80. package/python/biocore_model/tests/test_connection_bids.py +69 -0
  81. package/python/biocore_model/tests/test_core_bridge.py +88 -0
  82. package/python/biocore_model/tests/test_core_bridge_episode.py +14 -0
  83. package/python/biocore_model/tests/test_core_bridge_forget.py +17 -0
  84. package/python/biocore_model/tests/test_core_bridge_identity.py +15 -0
  85. package/python/biocore_model/tests/test_core_bridge_media.py +17 -0
  86. package/python/biocore_model/tests/test_core_bridge_relation.py +17 -0
  87. package/python/biocore_model/tests/test_core_bridge_summary.py +17 -0
  88. package/python/biocore_model/tests/test_core_client.py +156 -0
  89. package/python/biocore_model/tests/test_core_commands.py +146 -0
  90. package/python/biocore_model/tests/test_core_mode.py +161 -0
  91. package/python/biocore_model/tests/test_core_state.py +240 -0
  92. package/python/biocore_model/tests/test_criticality.py +61 -0
  93. package/python/biocore_model/tests/test_cross_session_copy.py +67 -0
  94. package/python/biocore_model/tests/test_debug_values.py +169 -0
  95. package/python/biocore_model/tests/test_defense_mechanisms.py +177 -0
  96. package/python/biocore_model/tests/test_delta_budget.py +157 -0
  97. package/python/biocore_model/tests/test_diag_trace.py +201 -0
  98. package/python/biocore_model/tests/test_directives_compat.py +698 -0
  99. package/python/biocore_model/tests/test_dissonance.py +97 -0
  100. package/python/biocore_model/tests/test_drift_sign.py +87 -0
  101. package/python/biocore_model/tests/test_embedding_fallback.py +277 -0
  102. package/python/biocore_model/tests/test_embedding_local.py +279 -0
  103. package/python/biocore_model/tests/test_embedding_mode.py +175 -0
  104. package/python/biocore_model/tests/test_embedding_modes.py +342 -0
  105. package/python/biocore_model/tests/test_emotion_geometry.py +114 -0
  106. package/python/biocore_model/tests/test_emotion_probe.py +214 -0
  107. package/python/biocore_model/tests/test_emotion_probe_concurrency.py +128 -0
  108. package/python/biocore_model/tests/test_entity_graph.py +198 -0
  109. package/python/biocore_model/tests/test_entity_resolve.py +446 -0
  110. package/python/biocore_model/tests/test_envy.py +173 -0
  111. package/python/biocore_model/tests/test_exit_or_silence.py +209 -0
  112. package/python/biocore_model/tests/test_expr_guard.py +1 -0
  113. package/python/biocore_model/tests/test_expr_intent.py +66 -0
  114. package/python/biocore_model/tests/test_expr_state.py +66 -0
  115. package/python/biocore_model/tests/test_expr_template.py +71 -0
  116. package/python/biocore_model/tests/test_expr_tone.py +41 -0
  117. package/python/biocore_model/tests/test_extreme_scenarios.py +147 -0
  118. package/python/biocore_model/tests/test_feedback_flow.py +185 -0
  119. package/python/biocore_model/tests/test_feedback_guard.py +65 -0
  120. package/python/biocore_model/tests/test_feedback_llm.py +59 -0
  121. package/python/biocore_model/tests/test_feedback_llm_inferred.py +140 -0
  122. package/python/biocore_model/tests/test_feedback_memory.py +63 -0
  123. package/python/biocore_model/tests/test_feedback_user.py +61 -0
  124. package/python/biocore_model/tests/test_frustration_curve.py +66 -0
  125. package/python/biocore_model/tests/test_global_bio_state.py +116 -0
  126. package/python/biocore_model/tests/test_governance.py +181 -0
  127. package/python/biocore_model/tests/test_granularity.py +133 -0
  128. package/python/biocore_model/tests/test_graph_extract.py +224 -0
  129. package/python/biocore_model/tests/test_graph_recall.py +226 -0
  130. package/python/biocore_model/tests/test_graph_sqlite.py +298 -0
  131. package/python/biocore_model/tests/test_growth.py +301 -0
  132. package/python/biocore_model/tests/test_intent_render.py +60 -0
  133. package/python/biocore_model/tests/test_intero_regulation.py +224 -0
  134. package/python/biocore_model/tests/test_intero_style.py +146 -0
  135. package/python/biocore_model/tests/test_item3_numeric.py +105 -0
  136. package/python/biocore_model/tests/test_item6_numeric.py +116 -0
  137. package/python/biocore_model/tests/test_layered_memory.py +174 -0
  138. package/python/biocore_model/tests/test_leak_guard.py +75 -0
  139. package/python/biocore_model/tests/test_learning_probe.py +93 -0
  140. package/python/biocore_model/tests/test_life_loop.py +222 -0
  141. package/python/biocore_model/tests/test_life_loop_10s.py +131 -0
  142. package/python/biocore_model/tests/test_local_embedding_health.py +258 -0
  143. package/python/biocore_model/tests/test_memory_affect_encoding.py +79 -0
  144. package/python/biocore_model/tests/test_memory_backend.py +332 -0
  145. package/python/biocore_model/tests/test_memory_dedup.py +316 -0
  146. package/python/biocore_model/tests/test_memory_dual.py +219 -0
  147. package/python/biocore_model/tests/test_memory_dual_wiring.py +219 -0
  148. package/python/biocore_model/tests/test_memory_feedback.py +241 -0
  149. package/python/biocore_model/tests/test_memory_fixes.py +84 -0
  150. package/python/biocore_model/tests/test_memory_forgetting.py +58 -0
  151. package/python/biocore_model/tests/test_memory_inject.py +211 -0
  152. package/python/biocore_model/tests/test_memory_mood_recall.py +118 -0
  153. package/python/biocore_model/tests/test_memory_store.py +372 -0
  154. package/python/biocore_model/tests/test_metacog.py +103 -0
  155. package/python/biocore_model/tests/test_migrate.py +245 -0
  156. package/python/biocore_model/tests/test_migrate_store.py +194 -0
  157. package/python/biocore_model/tests/test_mind_wander.py +65 -0
  158. package/python/biocore_model/tests/test_mood.py +35 -0
  159. package/python/biocore_model/tests/test_narrative.py +114 -0
  160. package/python/biocore_model/tests/test_needs_esteem.py +51 -0
  161. package/python/biocore_model/tests/test_needs_events.py +205 -0
  162. package/python/biocore_model/tests/test_needs_sleep.py +54 -0
  163. package/python/biocore_model/tests/test_noise_bank.py +54 -0
  164. package/python/biocore_model/tests/test_numeric_schema.py +98 -0
  165. package/python/biocore_model/tests/test_output_protocol.py +65 -0
  166. package/python/biocore_model/tests/test_page_debug.py +147 -0
  167. package/python/biocore_model/tests/test_page_status_autonomy.py +215 -0
  168. package/python/biocore_model/tests/test_pipeline.py +345 -0
  169. package/python/biocore_model/tests/test_pipeline_api.py +192 -0
  170. package/python/biocore_model/tests/test_pipeline_mode.py +222 -0
  171. package/python/biocore_model/tests/test_plugin_config.py +85 -0
  172. package/python/biocore_model/tests/test_policy_learning.py +116 -0
  173. package/python/biocore_model/tests/test_possible_selves.py +88 -0
  174. package/python/biocore_model/tests/test_predictor.py +227 -0
  175. package/python/biocore_model/tests/test_preference_drift.py +142 -0
  176. package/python/biocore_model/tests/test_preset.py +142 -0
  177. package/python/biocore_model/tests/test_proactive_context.py +70 -0
  178. package/python/biocore_model/tests/test_proactive_history.py +90 -0
  179. package/python/biocore_model/tests/test_proactive_memory.py +141 -0
  180. package/python/biocore_model/tests/test_proactive_send_record.py +88 -0
  181. package/python/biocore_model/tests/test_probe_contract.py +71 -0
  182. package/python/biocore_model/tests/test_prospective.py +154 -0
  183. package/python/biocore_model/tests/test_provider_fallback.py +89 -0
  184. package/python/biocore_model/tests/test_reconstruction.py +111 -0
  185. package/python/biocore_model/tests/test_regulation_strategies.py +228 -0
  186. package/python/biocore_model/tests/test_relation_stage.py +114 -0
  187. package/python/biocore_model/tests/test_relationship_stage.py +59 -0
  188. package/python/biocore_model/tests/test_release_140.py +55 -0
  189. package/python/biocore_model/tests/test_repair_after_boundary.py +249 -0
  190. package/python/biocore_model/tests/test_reply_limit.py +86 -0
  191. package/python/biocore_model/tests/test_reply_limit_persist.py +48 -0
  192. package/python/biocore_model/tests/test_reply_split.py +52 -0
  193. package/python/biocore_model/tests/test_reply_split_astrbot.py +66 -0
  194. package/python/biocore_model/tests/test_reply_split_inject.py +53 -0
  195. package/python/biocore_model/tests/test_rerank.py +229 -0
  196. package/python/biocore_model/tests/test_rollback.py +175 -0
  197. package/python/biocore_model/tests/test_runtime.py +175 -0
  198. package/python/biocore_model/tests/test_runtime_active.py +205 -0
  199. package/python/biocore_model/tests/test_runtime_pipeline.py +1583 -0
  200. package/python/biocore_model/tests/test_runtime_proactive.py +231 -0
  201. package/python/biocore_model/tests/test_scheduler.py +79 -0
  202. package/python/biocore_model/tests/test_secrecy.py +105 -0
  203. package/python/biocore_model/tests/test_self_esteem.py +45 -0
  204. package/python/biocore_model/tests/test_self_intent.py +347 -0
  205. package/python/biocore_model/tests/test_self_interest.py +183 -0
  206. package/python/biocore_model/tests/test_self_layer_wiring.py +198 -0
  207. package/python/biocore_model/tests/test_self_verification.py +113 -0
  208. package/python/biocore_model/tests/test_situation.py +142 -0
  209. package/python/biocore_model/tests/test_sleep_circadian.py +41 -0
  210. package/python/biocore_model/tests/test_social_pain.py +49 -0
  211. package/python/biocore_model/tests/test_social_repair.py +67 -0
  212. package/python/biocore_model/tests/test_social_theory.py +78 -0
  213. package/python/biocore_model/tests/test_source_monitoring.py +204 -0
  214. package/python/biocore_model/tests/test_speak_like_group.py +36 -0
  215. package/python/biocore_model/tests/test_stages.py +226 -0
  216. package/python/biocore_model/tests/test_state_tag.py +86 -0
  217. package/python/biocore_model/tests/test_template_expr.py +65 -0
  218. package/python/biocore_model/tests/test_text_inference.py +70 -0
  219. package/python/biocore_model/tests/test_theta_params.py +115 -0
  220. package/python/biocore_model/tests/test_topic_control.py +200 -0
  221. package/python/biocore_model/tests/test_turn_memory.py +105 -0
  222. package/python/biocore_model/tests/test_turn_memory_panel.py +174 -0
  223. package/python/biocore_model/tests/test_ui_settings.py +84 -0
  224. package/python/biocore_model/tests/test_ui_settings_authority.py +179 -0
  225. package/python/biocore_model/tests/test_user_appraisal_async.py +155 -0
  226. package/python/biocore_model/tests/test_user_classify.py +122 -0
  227. package/python/biocore_model/tests/test_value_order.py +117 -0
  228. package/python/biocore_model/tests/test_voice_bank.py +105 -0
  229. package/python/biocore_model/tests/test_waiting_sense.py +216 -0
  230. package/python/biocore_model/tests/test_x_directives.py +101 -0
  231. package/python/biocore_model/tests/ui_compat_check.mjs +22 -0
  232. package/python/biocore_model/tests/view_mode_check.mjs +17 -0
  233. package/python/biocore_runtime/autonomy.py +208 -0
  234. package/python/biocore_runtime/core_state.py +302 -0
  235. package/python/biocore_runtime/governance.py +495 -0
  236. package/python/biocore_runtime/growth.py +528 -0
  237. package/python/biocore_runtime/pipeline.py +850 -23
  238. package/python/biocore_runtime/relationship_state.py +214 -0
  239. package/python/biocore_runtime/self_layer.py +103 -0
  240. package/python/biocore_runtime/service.py +162 -0
  241. package/python/biocore_runtime/standby.py +472 -0
  242. package/ui/panel-layout.js +83 -39
  243. package/ui/panel.css +885 -743
  244. package/ui/panel.html +356 -296
  245. package/ui/panel.js +739 -865
@@ -0,0 +1,214 @@
1
+ # -*- coding: utf-8 -*-
2
+ """task-222: relationship governance for DSH (accumulation / memory / attachment).
3
+
4
+ The shared relationship file already stores trust/closeness/conflict. This
5
+ module adds the missing P0 pieces:
6
+
7
+ accumulation per-message signals -> bounded deltas (warmth, disclosure,
8
+ conflict, repair), applied by pipeline.relationship_update
9
+ memory <store>/relationship_memory.json keeps the last N notable
10
+ events (kind/ts/text preview/delta), append-only
11
+ attachment style + activation derived with biocore_model
12
+ attachment_state / attachment_regulation (pure, optional)
13
+ repair apology-style turns lower conflict and count as repairs
14
+
15
+ Never deletes relationship history and never writes memory entries.
16
+ """
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ import time
21
+ from pathlib import Path
22
+
23
+ try:
24
+ from biocore_model import attachment_regulation as _attach_reg
25
+ except Exception: # pragma: no cover - stripped checkout
26
+ _attach_reg = None
27
+ try:
28
+ from biocore_model import attachment_state as _attach_state
29
+ except Exception: # pragma: no cover
30
+ _attach_state = None
31
+
32
+ MEMORY_FILE = "relationship_memory.json"
33
+ EVENT_LIMIT = 50
34
+ SCHEMA = 1
35
+
36
+ _CONFLICT = ("生气", "讨厌", "闭嘴", "滚", "烦", "失望", "吵架", "别说了",
37
+ "你错了", "没用", "不想理")
38
+ _REPAIR = ("对不起", "抱歉", "是我不好", "别生气", "原谅", "我错了",
39
+ "不好意思", "不是那个意思")
40
+ _WARMTH = ("谢谢", "喜欢", "想你", "抱抱", "辛苦", "开心", "爱你", "陪我",
41
+ "在吗", "晚安")
42
+ _TRUST = ("相信", "告诉你", "秘密", "说实话", "只跟你说", "坦白")
43
+ _DISCLOSE = ("我其实", "我觉得", "我害怕", "我担心", "我一直")
44
+
45
+ DELTAS = {
46
+ "warmth": {"trust": 0.004, "closeness": 0.010},
47
+ "conflict": {"trust": -0.012, "closeness": -0.004, "conflict": 0.020},
48
+ "repair": {"trust": 0.008, "closeness": 0.004, "conflict": -0.020},
49
+ "trust": {"trust": 0.008, "closeness": 0.002},
50
+ "disclose": {"trust": 0.006, "closeness": 0.006},
51
+ }
52
+
53
+
54
+ def _num(value, default=0.0):
55
+ try:
56
+ number = float(value)
57
+ except Exception:
58
+ return default
59
+ return number if number == number else default
60
+
61
+
62
+ def _root(store):
63
+ if isinstance(store, dict):
64
+ raw = store.get("root") or store.get("store") or store.get("path")
65
+ else:
66
+ raw = store
67
+ text = str(raw or "").strip()
68
+ return Path(text) if text else None
69
+
70
+
71
+ def _data_dir(store):
72
+ root = _root(store)
73
+ if root is None:
74
+ return None
75
+ return root.parent if root.name == "memory" else root
76
+
77
+
78
+ def enabled(store):
79
+ data_dir = _data_dir(store)
80
+ raw = {}
81
+ if data_dir is not None:
82
+ try:
83
+ raw = json.loads((data_dir / "flags.json").read_text(encoding="utf-8"))
84
+ except Exception:
85
+ raw = {}
86
+ if not isinstance(raw, dict):
87
+ raw = {}
88
+ return raw.get("relationship_governance", True) is not False
89
+
90
+
91
+ def signals(text):
92
+ """One user message -> at most one kind plus its bounded deltas."""
93
+ value = str(text or "")
94
+ if any(word in value for word in _REPAIR):
95
+ kind = "repair"
96
+ elif any(word in value for word in _CONFLICT):
97
+ kind = "conflict"
98
+ elif any(word in value for word in _WARMTH):
99
+ kind = "warmth"
100
+ elif any(word in value for word in _TRUST):
101
+ kind = "trust"
102
+ elif any(word in value for word in _DISCLOSE):
103
+ kind = "disclose"
104
+ else:
105
+ return {"kind": "neutral", "deltas": {}}
106
+ return {"kind": kind, "deltas": dict(DELTAS[kind])}
107
+
108
+
109
+ def style_for(trust, closeness, conflict, repairs=0):
110
+ trust = _num(trust, 0.5)
111
+ closeness = _num(closeness, 0.5)
112
+ conflict = _num(conflict, 0.0)
113
+ if conflict >= 0.45 and trust < 0.55:
114
+ return "anxious"
115
+ if trust < 0.35 and closeness < 0.40:
116
+ return "avoidant"
117
+ if trust >= 0.55 and conflict < 0.25:
118
+ return "secure"
119
+ return "mixed"
120
+
121
+
122
+ def activation_for(style, conflict=0.0, current=0.0, responded=False, hours=0.0):
123
+ """Attachment activation via the pure model modules (safe fallback)."""
124
+ style = style if style in ("secure", "anxious", "avoidant") else "secure"
125
+ try:
126
+ if _attach_reg is not None:
127
+ return dict(_attach_reg.activation(
128
+ style, threat=min(1.0, max(0.0, _num(conflict))),
129
+ current=_num(current), responded=bool(responded),
130
+ hours=max(0.0, _num(hours))))
131
+ except Exception:
132
+ pass
133
+ return {"style": style, "activation": round(min(1.0, max(0.0, _num(current))), 4),
134
+ "mode": "comfort", "drive_mult": 1.0, "tone": "seek_comfort"}
135
+
136
+
137
+ def memory_path(store):
138
+ root = _root(store)
139
+ return (root / MEMORY_FILE) if root is not None else None
140
+
141
+
142
+ def read_memory(store):
143
+ path = memory_path(store)
144
+ try:
145
+ if path is None or not path.exists():
146
+ return {"schema": SCHEMA, "events": []}
147
+ data = json.loads(path.read_text(encoding="utf-8"))
148
+ if not isinstance(data, dict):
149
+ return {"schema": SCHEMA, "events": []}
150
+ events = [row for row in (data.get("events") or []) if isinstance(row, dict)]
151
+ return {"schema": SCHEMA, "events": events[-EVENT_LIMIT:]}
152
+ except Exception:
153
+ return {"schema": SCHEMA, "events": []}
154
+
155
+
156
+ def _write_memory(store, doc):
157
+ path = memory_path(store)
158
+ if path is None:
159
+ return False
160
+ try:
161
+ path.parent.mkdir(parents=True, exist_ok=True)
162
+ tmp = path.with_name(path.name + ".tmp")
163
+ tmp.write_text(json.dumps(doc, ensure_ascii=False, indent=2),
164
+ encoding="utf-8")
165
+ tmp.replace(path)
166
+ return True
167
+ except Exception:
168
+ return False
169
+
170
+
171
+ def record_event(store, kind, text, deltas, now=None):
172
+ """Append one notable relationship event (no-op for neutral turns)."""
173
+ if kind in ("", "neutral"):
174
+ return None
175
+ doc = read_memory(store)
176
+ row = {"ts": round(_num(now, time.time()), 3),
177
+ "kind": str(kind)[:16],
178
+ "text": str(text or "")[:40],
179
+ "delta": {key: round(_num(value, 0.0), 4)
180
+ for key, value in (deltas or {}).items()}}
181
+ events = (doc.get("events") or []) + [row]
182
+ doc["events"] = events[-EVENT_LIMIT:]
183
+ _write_memory(store, doc)
184
+ return row
185
+
186
+
187
+ def summary(store, relationship=None):
188
+ """Counters for /state: accumulation totals + attachment view."""
189
+ doc = read_memory(store)
190
+ events = doc.get("events") or []
191
+ rel = relationship if isinstance(relationship, dict) else {}
192
+ counts = {}
193
+ for row in events:
194
+ kind = str(row.get("kind") or "")
195
+ if kind:
196
+ counts[kind] = counts.get(kind, 0) + 1
197
+ style = str(rel.get("attachment_style") or "")
198
+ if not style:
199
+ style = style_for(rel.get("trust"), rel.get("closeness"),
200
+ rel.get("conflict"), counts.get("repair", 0))
201
+ view = activation_for(style, _num(rel.get("conflict"), 0.0))
202
+ last = events[-1] if events else {}
203
+ return {
204
+ "events": len(events),
205
+ "repairs": int(counts.get("repair", 0)),
206
+ "warmth": int(counts.get("warmth", 0)),
207
+ "conflicts": int(counts.get("conflict", 0)),
208
+ "attachment_style": view.get("style") or style,
209
+ "attachment_activation": round(_num(view.get("activation"), 0.0), 4),
210
+ "last_event": {"kind": str(last.get("kind") or ""),
211
+ "ts": round(_num(last.get("ts"), 0.0), 3),
212
+ "text": str(last.get("text") or "")},
213
+ "enabled": enabled(store),
214
+ }
@@ -0,0 +1,103 @@
1
+ # -*- coding: utf-8 -*-
2
+ """task-222: self layer (persona lock / narrative / drift) for the DSH core."""
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ import time
8
+ from pathlib import Path
9
+
10
+ IDENTITY_FILE = "identity.json"
11
+ NARRATIVE_LIMIT = 30
12
+ SCHEMA = 1
13
+
14
+
15
+ def _root(store):
16
+ if isinstance(store, dict):
17
+ raw = store.get("root") or store.get("store") or store.get("path")
18
+ else:
19
+ raw = store
20
+ text = str(raw or "").strip()
21
+ return Path(text) if text else None
22
+
23
+
24
+ def _data_dir(store):
25
+ root = _root(store)
26
+ if root is None:
27
+ return None
28
+ return root.parent if root.name == "memory" else root
29
+
30
+
31
+ def path_for(store):
32
+ data_dir = _data_dir(store)
33
+ return (data_dir / IDENTITY_FILE) if data_dir is not None else None
34
+
35
+
36
+ def _read(path):
37
+ try:
38
+ if path is None or not path.exists():
39
+ return {}
40
+ doc = json.loads(path.read_text(encoding="utf-8"))
41
+ return doc if isinstance(doc, dict) else {}
42
+ except Exception:
43
+ return {}
44
+
45
+
46
+ def _write(path, doc):
47
+ try:
48
+ path.parent.mkdir(parents=True, exist_ok=True)
49
+ tmp = path.with_name(path.name + ".tmp")
50
+ tmp.write_text(json.dumps(doc, ensure_ascii=False, indent=2),
51
+ encoding="utf-8")
52
+ tmp.replace(path)
53
+ return True
54
+ except Exception:
55
+ return False
56
+
57
+
58
+ def hash_persona(text):
59
+ raw = str(text or "").strip()
60
+ if not raw:
61
+ return ""
62
+ return hashlib.sha256(raw.encode("utf-8")).hexdigest()
63
+
64
+
65
+ def update(store, persona_hash="", narrative=None, now=None):
66
+ """Lock the persona hash, append one narrative line, detect drift."""
67
+ path = path_for(store)
68
+ if path is None:
69
+ return None
70
+ stamp = round(float(now or time.time()), 3)
71
+ doc = _read(path)
72
+ locked = str(doc.get("persona_hash") or "")
73
+ incoming = str(persona_hash or "").strip()
74
+ drift = bool(locked and incoming and locked != incoming)
75
+ if incoming and not locked:
76
+ locked = incoming
77
+ if incoming and drift:
78
+ history = [row for row in (doc.get("drift") or []) if isinstance(row, dict)]
79
+ history.append({"ts": stamp, "from": locked, "to": incoming})
80
+ doc["drift"] = history[-10:]
81
+ locked = incoming
82
+ lines = [row for row in (doc.get("narrative") or []) if isinstance(row, dict)]
83
+ if isinstance(narrative, dict) and str(narrative.get("text") or "").strip():
84
+ lines.append({"ts": stamp, "kind": str(narrative.get("kind") or "note")[:16],
85
+ "text": str(narrative.get("text"))[:60]})
86
+ doc.update({"schema": SCHEMA, "persona_hash": locked,
87
+ "persona_locked": bool(locked), "narrative": lines[-NARRATIVE_LIMIT:],
88
+ "updated_at": stamp, "drift_count": len(doc.get("drift") or [])})
89
+ _write(path, doc)
90
+ return doc
91
+
92
+
93
+ def summary(store):
94
+ doc = _read(path_for(store))
95
+ lines = [row for row in (doc.get("narrative") or []) if isinstance(row, dict)]
96
+ return {
97
+ "persona_locked": bool(doc.get("persona_locked")),
98
+ "persona_hash": str(doc.get("persona_hash") or "")[:12],
99
+ "drift_count": int(doc.get("drift_count") or 0),
100
+ "narrative_count": len(lines),
101
+ "last_narrative": (lines[-1].get("text") if lines else ""),
102
+ "updated_at": float(doc.get("updated_at") or 0.0),
103
+ }
@@ -0,0 +1,162 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ """task-223: persistent BioCore service (one process per DSH plugin lifetime).
4
+
5
+ Protocol: one JSON object per line on stdin, one JSON reply per line on stdout.
6
+
7
+ request {"id": <str>, "action": <str>, "payload": {...}}
8
+ reply {"id": <str>, "ok": true, "result": {...}}
9
+ {"id": <str>, "ok": false, "reason": "...", "detail": "..."}
10
+
11
+ The action is handed to biocore_runtime.pipeline.dispatch, so every policy
12
+ stays in the existing module. A bad request or an exception is answered and
13
+ the loop keeps running; SIGTERM/SIGINT stop it cleanly. No third-party deps.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ import argparse
18
+ import json
19
+ import os
20
+ import signal
21
+ import sys
22
+ import threading
23
+ import time
24
+ import traceback
25
+ from pathlib import Path
26
+
27
+ _PLUGIN_ROOT = str(Path(__file__).resolve().parents[1])
28
+ if _PLUGIN_ROOT not in sys.path:
29
+ sys.path.insert(0, _PLUGIN_ROOT)
30
+ _ADAPTER_DIR = str(os.environ.get("BIOCORE_ADAPTER_DIR") or "").strip()
31
+ if _ADAPTER_DIR and _ADAPTER_DIR not in sys.path:
32
+ sys.path.insert(0, _ADAPTER_DIR)
33
+
34
+ from biocore_runtime import pipeline # noqa: E402
35
+
36
+ STOP = False
37
+ _IDLE_EVENT = threading.Event()
38
+
39
+
40
+ def _install_signals():
41
+ def _stop(signum, frame):
42
+ global STOP
43
+ STOP = True
44
+ _IDLE_EVENT.set()
45
+ for name in ("SIGTERM", "SIGINT"):
46
+ handler = getattr(signal, name, None)
47
+ if handler is None:
48
+ continue
49
+ try:
50
+ signal.signal(handler, _stop)
51
+ except Exception:
52
+ pass
53
+
54
+
55
+ def _reply(stream, req_id, ok, result=None, reason="", detail=""):
56
+ payload = {"id": req_id, "ok": bool(ok)}
57
+ if ok:
58
+ payload["result"] = result if result is not None else {}
59
+ else:
60
+ payload["reason"] = str(reason or "service_error")
61
+ if detail:
62
+ payload["detail"] = str(detail)[:300]
63
+ try:
64
+ stream.write(json.dumps(payload, ensure_ascii=False,
65
+ separators=(",", ":")) + "\n")
66
+ stream.flush()
67
+ except Exception:
68
+ pass
69
+
70
+
71
+ def handle(req):
72
+ """One request dict -> (ok, result, reason, detail); never raises."""
73
+ if not isinstance(req, dict):
74
+ return False, None, "bad_request", "request must be an object"
75
+ req_id = req.get("id")
76
+ action = str(req.get("action") or "").strip()
77
+ payload = req.get("payload")
78
+ payload = payload if isinstance(payload, dict) else {}
79
+ if not action:
80
+ return False, None, "bad_request", "missing action"
81
+ if action == "ping":
82
+ return True, {"pong": True, "echo_len": len(json.dumps(payload)),
83
+ "pid": os.getpid()}, "", ""
84
+ if action == "sleep":
85
+ try:
86
+ ms = max(0, min(5000, int(payload.get("ms") or 0)))
87
+ except Exception:
88
+ ms = 0
89
+ time.sleep(ms / 1000.0)
90
+ return True, {"slept": ms}, "", ""
91
+ try:
92
+ result = pipeline.dispatch(action, payload)
93
+ return True, result, "", ""
94
+ except Exception as error: # keep the service alive
95
+ detail = "%s: %s" % (type(error).__name__, str(error)[:200])
96
+ try:
97
+ traceback.print_exc(file=sys.stderr)
98
+ except Exception:
99
+ pass
100
+ return False, None, "service_error", detail
101
+
102
+
103
+ def _watchdog(idle_timeout):
104
+ """Exit when no request arrived for idle_timeout seconds (0 = never)."""
105
+ if not idle_timeout or idle_timeout <= 0:
106
+ return
107
+
108
+ def _run():
109
+ while not STOP:
110
+ fired = _IDLE_EVENT.wait(timeout=idle_timeout)
111
+ if fired:
112
+ return
113
+ if STOP:
114
+ return
115
+ try:
116
+ sys.stderr.write("__bioService idle_timeout\n")
117
+ sys.stderr.flush()
118
+ except Exception:
119
+ pass
120
+ os._exit(0)
121
+
122
+ thread = threading.Thread(target=_run, daemon=True)
123
+ thread.start()
124
+
125
+
126
+ def serve(stream_in=None, stream_out=None, idle_timeout=0.0):
127
+ global STOP
128
+ stdin = stream_in or sys.stdin
129
+ stdout = stream_out or sys.stdout
130
+ _watchdog(float(idle_timeout or 0.0))
131
+ for line in stdin:
132
+ if STOP:
133
+ break
134
+ line = str(line or "").strip()
135
+ if not line:
136
+ continue
137
+ _IDLE_EVENT.set()
138
+ _IDLE_EVENT.clear()
139
+ req_id = None
140
+ try:
141
+ request = json.loads(line)
142
+ if isinstance(request, dict):
143
+ req_id = request.get("id")
144
+ ok, result, reason, detail = handle(request)
145
+ except Exception as error:
146
+ ok, result, reason, detail = (False, None, "bad_json",
147
+ "%s: %s" % (type(error).__name__,
148
+ str(error)[:200]))
149
+ _reply(stdout, req_id, ok, result, reason, detail)
150
+ return 0
151
+
152
+
153
+ def main(argv=None):
154
+ parser = argparse.ArgumentParser(add_help=False)
155
+ parser.add_argument("--idle-timeout", type=float, default=0.0)
156
+ args, _unknown = parser.parse_known_args(list(argv or sys.argv[1:]))
157
+ _install_signals()
158
+ return serve(idle_timeout=args.idle_timeout)
159
+
160
+
161
+ if __name__ == "__main__":
162
+ raise SystemExit(main())