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
@@ -69,6 +69,8 @@ if _PLUGIN_ROOT not in sys.path:
69
69
 
70
70
  MAX_CHARS = 420
71
71
  TOP_K = 5
72
+ INJECT_MAX_K = 10 # inject_block may ask for more when rerank prunes
73
+ INJECT_MODES = ("query", "recent")
72
74
  HOURLY_CAP = 20
73
75
  MIN_CHARS = 8
74
76
  MAX_TEXT_CHARS = 2000
@@ -96,6 +98,43 @@ try:
96
98
  except Exception: # pragma: no cover
97
99
  _rerank_mod = None
98
100
 
101
+ try: # task-221: one global core state shared by every session
102
+ from . import core_state as _core_state
103
+ except Exception: # pragma: no cover
104
+ try:
105
+ from biocore_runtime import core_state as _core_state
106
+ except Exception:
107
+ _core_state = None
108
+
109
+ try: # task-223: memory governance + relationship accumulation
110
+ from . import governance as _governance
111
+ except Exception: # pragma: no cover
112
+ try:
113
+ from biocore_runtime import governance as _governance
114
+ except Exception:
115
+ _governance = None
116
+ try:
117
+ from . import relationship_state as _relationship_state
118
+ except Exception: # pragma: no cover
119
+ try:
120
+ from biocore_runtime import relationship_state as _relationship_state
121
+ except Exception:
122
+ _relationship_state = None
123
+ try: # task-222: self layer + autonomy
124
+ from . import self_layer as _self_layer
125
+ except Exception: # pragma: no cover
126
+ try:
127
+ from biocore_runtime import self_layer as _self_layer
128
+ except Exception:
129
+ _self_layer = None
130
+ try:
131
+ from . import autonomy as _autonomy
132
+ except Exception: # pragma: no cover
133
+ try:
134
+ from biocore_runtime import autonomy as _autonomy
135
+ except Exception:
136
+ _autonomy = None
137
+
99
138
  try:
100
139
  from biocore_model import sanitize as _sanitize
101
140
  except Exception: # pragma: no cover
@@ -378,16 +417,272 @@ def capture(session_id, role, text, store, config=None):
378
417
  return {"ok": True, "decision": decision, "entry": entry}
379
418
 
380
419
 
420
+ # ---------------------------------------------------------------- governance
421
+ def _concepts_of(text):
422
+ if _governance is None:
423
+ return []
424
+ try:
425
+ return _governance.extract_concepts(text)
426
+ except Exception:
427
+ return []
428
+
429
+
430
+ def _concept_filter(entries, query):
431
+ """Keep entries whose concepts intersect the query (fallback: all)."""
432
+ rows = [entry for entry in (entries or []) if isinstance(entry, dict)]
433
+ wanted = set(_concepts_of(query))
434
+ if not wanted:
435
+ return rows
436
+ narrowed = [entry for entry in rows
437
+ if wanted & set(entry.get("concepts") or [])]
438
+ return narrowed or rows
439
+
440
+
441
+ def _governance_tick(store, now=None):
442
+ """Decay/index/graph/aggregate sidecars; never deletes a memory."""
443
+ if _governance is None or not store:
444
+ return None
445
+ try:
446
+ return _governance.run(store, now=now)
447
+ except Exception:
448
+ return None
449
+
450
+
451
+ def _relationship_tick(store, user_text, now=None):
452
+ """Bounded trust/closeness/conflict accumulation + attachment/repair."""
453
+ if _relationship_state is None or not store:
454
+ return None
455
+ try:
456
+ if not _relationship_state.enabled(store):
457
+ return {"enabled": False, "skipped": True}
458
+ before = relationship_read(store)["relationship"]
459
+ signal = _relationship_state.signals(user_text)
460
+ deltas = signal.get("deltas") or {}
461
+ if deltas:
462
+ relationship_update(store, deltas=deltas, now=now)
463
+ after = relationship_read(store)["relationship"]
464
+ summary = _relationship_state.summary(store, after)
465
+ style = summary.get("attachment_style")
466
+ if style and style != after.get("attachment_style"):
467
+ relationship_update(store, attachment_style=style, now=now)
468
+ after = relationship_read(store)["relationship"]
469
+ _relationship_state.record_event(store, signal.get("kind"),
470
+ user_text, deltas, now=now)
471
+ return {"kind": signal.get("kind"), "deltas": deltas,
472
+ "trust": round(_num(after.get("trust"), 0.5), 4),
473
+ "closeness": round(_num(after.get("closeness"), 0.5), 4),
474
+ "conflict": round(_num(after.get("conflict"), 0.0), 4),
475
+ "delta_trust": round(_num(after.get("trust"), 0.5)
476
+ - _num(before.get("trust"), 0.5), 4),
477
+ "delta_closeness": round(_num(after.get("closeness"), 0.5)
478
+ - _num(before.get("closeness"), 0.5), 4),
479
+ "attachment_style": str(after.get("attachment_style") or style),
480
+ "summary": summary}
481
+ except Exception:
482
+ return None
483
+
484
+
485
+ def _identity_tick(store, narrative=None, now=None):
486
+ if _self_layer is None or not store:
487
+ return None
488
+ try:
489
+ cfg = _load_runtime_config(store)
490
+ persona_hash = _self_layer.hash_persona(cfg.get("persona_text"))
491
+ return _self_layer.update(store, persona_hash=persona_hash,
492
+ narrative=narrative, now=now)
493
+ except Exception:
494
+ return None
495
+
496
+
497
+ def _autonomy_tick(store, data=None, now=None):
498
+ if _autonomy is None or not store:
499
+ return None
500
+ try:
501
+ payload = data if isinstance(data, dict) else {}
502
+ switches = _effective_switches(store, payload.get("switches"))
503
+ if not switches.get("core_enabled", True) or not switches.get(
504
+ "autonomy", True):
505
+ return {"ok": True, "action": None, "reason": "disabled",
506
+ "metrics": _autonomy.metrics(store, now),
507
+ "limits": _autonomy.limits(store)}
508
+ core_path = _core_state_path(payload)
509
+ state = {}
510
+ if _core_state is not None and _core_state.exists(core_path):
511
+ doc = _core_state.load(core_path)
512
+ state = {"core": _core_state.core_of(doc),
513
+ "intero": doc.get("intero")}
514
+ elif isinstance(payload.get("state"), dict):
515
+ state = payload["state"]
516
+ return _autonomy.decide(
517
+ store, now=now,
518
+ session_id=str(payload.get("session_id") or "autonomy"),
519
+ state=state,
520
+ last_human_ts=_num(payload.get("last_human_ts"), 0.0),
521
+ silence_minutes=_num(payload.get("silence_minutes"), 0.0))
522
+ except Exception:
523
+ return None
524
+
525
+
381
526
  # ---------------------------------------------------------------- inject
382
- def inject_block(session_id, store, max_chars=MAX_CHARS, top_k=TOP_K):
383
- """Top-k session memories as one bounded Chinese prompt block."""
527
+ def _store_data_dir(store):
528
+ root = _store_root(store)
529
+ if root is None:
530
+ return None
531
+ return root.parent if root.name == "memory" else root
532
+
533
+
534
+ def _data_flags(store):
535
+ """<dataDir>/flags.json booleans (learning switches live next to memory/).
536
+
537
+ task-221口径: DSH learning switches default ON; an explicit file value wins.
538
+ """
539
+ merged = {"mood_recall": True, "affect_encoding": True,
540
+ "memory_feedback": True, "preference_drift": True}
541
+ data_dir = _store_data_dir(store)
542
+ if data_dir is None:
543
+ return merged
544
+ raw = _load_json(data_dir / "flags.json", {})
545
+ for key, value in raw.items():
546
+ if isinstance(value, bool):
547
+ merged[str(key)] = value
548
+ cfg = _load_runtime_config(store)
549
+ switches = cfg.get("switches") if isinstance(cfg.get("switches"), dict) else {}
550
+ for key, value in switches.items():
551
+ if isinstance(value, bool):
552
+ merged[str(key)] = value
553
+ return merged
554
+
555
+
556
+ def _inject_engine(store, entries):
557
+ """MemoryEngine over this session's entries, with the env embedder."""
558
+ try:
559
+ from biocore_memory.engine import MemoryEngine
560
+ from biocore_memory.recall.embedding import load_provider
561
+ from biocore_memory.store.json_store import MemoryStore
562
+ except Exception:
563
+ return None
564
+ embedder = None
565
+ try:
566
+ cfg = _load_runtime_config(store)
567
+ emb = cfg.get("embedding") if isinstance(cfg.get("embedding"), dict) else {}
568
+ provider_cfg = {}
569
+ if emb:
570
+ provider_cfg = {
571
+ "embedding_mode": emb.get("mode") or "auto",
572
+ "embedding_base_url": emb.get("base_url") or "",
573
+ "embedding_model": emb.get("model") or "",
574
+ "embedding_api_key": emb.get("api_key") or "",
575
+ }
576
+ embedder = load_provider(provider_cfg)
577
+ except Exception:
578
+ embedder = None
579
+ try:
580
+ memory = MemoryStore(doc={"entries": [dict(item) for item in entries
581
+ if isinstance(item, dict)]})
582
+ return MemoryEngine(store=memory, embedder=embedder,
583
+ config={"flags": _data_flags(store)})
584
+ except Exception:
585
+ return None
586
+
587
+
588
+ def _rerank_entries(query, entries, rerank_config):
589
+ """Reorder candidates by the optional rerank API; None keeps local order."""
590
+ if _rerank_mod is None or not isinstance(rerank_config, dict):
591
+ return None
592
+ if not rerank_config.get("rerank_enabled"):
593
+ return None
594
+ try:
595
+ texts = [str(entry.get("text") or "") for entry in entries]
596
+ ranked = _rerank_mod.rerank_documents(query, texts, rerank_config)
597
+ except Exception:
598
+ return None
599
+ if not ranked:
600
+ return None
601
+ ordered = []
602
+ for item in ranked:
603
+ try:
604
+ index = int(item.get("index", -1))
605
+ except Exception:
606
+ continue
607
+ if 0 <= index < len(entries) and entries[index] not in ordered:
608
+ ordered.append(entries[index])
609
+ for entry in entries:
610
+ if entry not in ordered:
611
+ ordered.append(entry)
612
+ return ordered or None
613
+
614
+
615
+ def _query_entries(store, session_id, query, top_k, rerank_config):
616
+ """engine.recall(query) over the injected candidates; None means fallback."""
617
+ entries = _visible(load_memory(store, session_id).get("entries"))
618
+ if not entries:
619
+ return None
620
+ engine = _inject_engine(store, entries)
621
+ if engine is None:
622
+ return None
623
+ rows = engine.recall(str(query)[:MAX_TEXT_CHARS], budget=top_k)
624
+ if not isinstance(rows, list) or not rows:
625
+ return None
626
+ picked = [row.get("entry") for row in rows
627
+ if isinstance(row, dict) and isinstance(row.get("entry"), dict)]
628
+ if not picked:
629
+ return None
630
+ # task-223: concept filter (entries written with concepts only).
631
+ narrowed = _concept_filter(picked, query)
632
+ if 0 < len(narrowed) < len(picked):
633
+ picked = narrowed
634
+ if _governance is not None:
635
+ try:
636
+ _governance.note_concept_filter(store, len(picked))
637
+ except Exception:
638
+ pass
639
+ if len(picked) > 1:
640
+ reordered = _rerank_entries(query, picked, rerank_config)
641
+ if reordered:
642
+ picked = reordered
643
+ # task-223: index hit + decay weight (cold memories sink, never deleted).
644
+ if _governance is not None:
645
+ try:
646
+ hits = set(_governance.lookup(store, query))
647
+ if hits:
648
+ _governance.note_index_hit(store)
649
+ if hits:
650
+ picked.sort(key=lambda entry: (
651
+ 1 if str(entry.get("id") or "") in hits else 0,
652
+ _governance.weight_of(store, str(entry.get("id") or "")),
653
+ _num(entry.get("importance"), 0.5)), reverse=True)
654
+ except Exception:
655
+ pass
656
+ return picked[:top_k]
657
+
658
+
659
+ def inject_block(session_id, store, max_chars=MAX_CHARS, top_k=TOP_K,
660
+ query="", mode="query", rerank_config=None):
661
+ """Top-k session memories as one bounded Chinese prompt block.
662
+
663
+ mode="query" recall this turn's words (engine.recall + env embedder,
664
+ optional rerank); any failure falls back to recent.
665
+ mode="recent" keep the pin+ts ordering.
666
+ """
384
667
  session_id = sanitize_session_id(session_id)
385
668
  budget = _int_param(max_chars, MAX_CHARS, 0, MAX_CHARS)
386
- limit = _int_param(top_k, TOP_K, 1, TOP_K)
387
- entries = sorted(_visible(load_memory(store, session_id).get("entries")),
388
- key=lambda entry: (1 if entry.get("pin") else 0,
389
- _num(entry.get("ts"), 0.0)),
390
- reverse=True)[:limit]
669
+ limit = _int_param(top_k, TOP_K, 1, INJECT_MAX_K)
670
+ text_query = clean_text(query, MAX_TEXT_CHARS)
671
+ chosen = str(mode or "query").strip().lower()
672
+ entries = []
673
+ if chosen == "query" and text_query:
674
+ try:
675
+ picked = _query_entries(store, session_id, text_query, limit,
676
+ rerank_config)
677
+ except Exception:
678
+ picked = None
679
+ if picked:
680
+ entries = list(picked)
681
+ if not entries:
682
+ entries = sorted(_visible(load_memory(store, session_id).get("entries")),
683
+ key=lambda entry: (1 if entry.get("pin") else 0,
684
+ _num(entry.get("ts"), 0.0)),
685
+ reverse=True)[:limit]
391
686
  header = "你记得这些事:"
392
687
  ids = []
393
688
  if not entries or budget <= len(header) + 3:
@@ -591,6 +886,7 @@ def _new_entry(text, role, tags, now):
591
886
  "pin": False,
592
887
  "importance": 0.5,
593
888
  "hits": 1,
889
+ "concepts": _concepts_of(text),
594
890
  }
595
891
 
596
892
 
@@ -1558,6 +1854,365 @@ def apply_persona_to_session(session_id, payload):
1558
1854
  return state
1559
1855
 
1560
1856
 
1857
+
1858
+
1859
+ # ---------------------------------------------------------------- core state
1860
+ def _core_state_path(payload):
1861
+ """<dataDir>/core_state.json for any payload (store = <dataDir>/memory)."""
1862
+ if _core_state is None:
1863
+ return None
1864
+ data = payload if isinstance(payload, dict) else {}
1865
+ try:
1866
+ return _core_state.path_for(data.get("store"),
1867
+ data.get("data_dir") or data.get("dataDir"))
1868
+ except Exception:
1869
+ return None
1870
+
1871
+
1872
+ def _intero_from_core(doc, prev, dt_hours, now):
1873
+ """Recompute intero from the global body, using its own intero as prev."""
1874
+ body = dict(doc) if isinstance(doc, dict) else {}
1875
+ merged = {"core": body}
1876
+ if isinstance(prev, dict) and prev:
1877
+ merged["core"] = dict(body, intero=prev)
1878
+ merged["intero"] = dict(prev)
1879
+ return compute_intero(merged, dt_hours=dt_hours or 0.0, now=now)
1880
+
1881
+
1882
+ def _state_deltas(payload):
1883
+ """This turn's numeric deltas: direct dict, or diagnostics before/after."""
1884
+ data = payload if isinstance(payload, dict) else {}
1885
+ out = {}
1886
+
1887
+ def add(key, value):
1888
+ if key not in _CORE_FIELDS:
1889
+ return
1890
+ try:
1891
+ out[key] = out.get(key, 0.0) + float(value)
1892
+ except Exception:
1893
+ pass
1894
+
1895
+ for source_key in ("delta", "deltas", "state_delta"):
1896
+ raw = data.get(source_key)
1897
+ if isinstance(raw, dict):
1898
+ for key, value in raw.items():
1899
+ add(str(key), value)
1900
+ elif isinstance(raw, list):
1901
+ for item in raw:
1902
+ if not isinstance(item, dict):
1903
+ continue
1904
+ add(str(item.get("field") or item.get("key") or ""),
1905
+ _num(item.get("after"), 0.0) - _num(item.get("before"), 0.0))
1906
+ return {key: round(value, 4) for key, value in out.items()
1907
+ if abs(value) > 1e-9}
1908
+
1909
+
1910
+ _CORE_FIELDS = ("v", "a", "stress", "energy", "social", "social_pain",
1911
+ "trust", "closeness", "conflict")
1912
+
1913
+
1914
+ def _recall_count(store, session_id):
1915
+ """Recalled memory hits for this session (0 when traces are empty)."""
1916
+ try:
1917
+ total = 0
1918
+ for row in trace_read(store, session_id, TRACE_KEEP):
1919
+ memory = row.get("memory") if isinstance(row.get("memory"), dict) else {}
1920
+ ids = memory.get("recalled_ids")
1921
+ if isinstance(ids, list):
1922
+ total += len(ids)
1923
+ return total
1924
+ except Exception:
1925
+ return 0
1926
+
1927
+
1928
+ def _proactive_sent_today(store):
1929
+ """Messages delivered by the proactive path today (0 when none)."""
1930
+ try:
1931
+ root = _store_root(store)
1932
+ if root is None:
1933
+ return 0
1934
+ data = _load_json(root / "proactive.json", {})
1935
+ sent = data.get("sent") if isinstance(data.get("sent"), list) else []
1936
+ now = time.time()
1937
+ start = time.mktime(time.localtime(now)[:3] + (0, 0, 0, 0, 0, -1))
1938
+ return sum(1 for item in sent if _num(item, 0.0) / 1000.0 >= start)
1939
+ except Exception:
1940
+ return 0
1941
+
1942
+
1943
+ def _series_payload(store, payload=None):
1944
+ """Newest <=24 points from the global history + growth_history.jsonl."""
1945
+ data = payload if isinstance(payload, dict) else {}
1946
+ limit = _int_param(data.get("limit"), 24, 1, 288)
1947
+ rows = []
1948
+ try:
1949
+ rows = _core_state.points(_core_state_path(data), limit) if _core_state else []
1950
+ except Exception:
1951
+ rows = []
1952
+ growth = []
1953
+ try:
1954
+ root = _store_root(store)
1955
+ path = (root / "growth_history.jsonl") if root is not None else None
1956
+ if path is not None and path.exists():
1957
+ for line in path.read_text(encoding="utf-8").splitlines():
1958
+ line = line.strip()
1959
+ if not line:
1960
+ continue
1961
+ try:
1962
+ item = json.loads(line)
1963
+ except Exception:
1964
+ continue
1965
+ if isinstance(item, dict) and _num(item.get("ts"), 0.0) > 0:
1966
+ growth.append({"ts": round(_num(item.get("ts")), 3),
1967
+ "trust": round(_num(item.get("trust"), 0.5), 4),
1968
+ "closeness": round(_num(item.get("closeness"),
1969
+ 0.5), 4)})
1970
+ except Exception:
1971
+ growth = []
1972
+ if growth:
1973
+ known = {round(_num(row.get("ts"), 0.0), 0) for row in rows}
1974
+ for item in growth[-limit:]:
1975
+ if round(_num(item.get("ts"), 0.0), 0) not in known:
1976
+ rows.append(item)
1977
+ rows.sort(key=lambda row: _num(row.get("ts"), 0.0))
1978
+ rows = rows[-limit:]
1979
+ series = {key: [row.get(key) for row in rows
1980
+ if isinstance(row, dict) and row.get(key) is not None]
1981
+ for key in ("ts", "v", "tension", "warmth", "heaviness",
1982
+ "clarity", "trust", "closeness")}
1983
+ return {"ok": True, "series": series, "points": rows,
1984
+ "source": "core_state" if _core_state else "unavailable"}
1985
+
1986
+
1987
+ _RUNTIME_CONFIG = {}
1988
+ CORE_CONFIG_FILE = "core_config.json"
1989
+ CORE_DEFAULTS = {
1990
+ # contract: everything core-side is on; only external/irreversible off.
1991
+ "core_enabled": True,
1992
+ "mood_recall": True, "affect_encoding": True,
1993
+ "memory_feedback": True, "preference_drift": True,
1994
+ "governance": True, "index": True, "consolidate": True,
1995
+ "evict": True, "evict_delete": False,
1996
+ "relationship_governance": True,
1997
+ "self_layer": True, "motivation": True,
1998
+ "autonomy": True, "standby": True,
1999
+ "safety_enabled": False, "boundary_enabled": False,
2000
+ "crisis_safety_enabled": False,
2001
+ "proactive_send": True,
2002
+ "recall_mode": "query",
2003
+ }
2004
+
2005
+
2006
+ def _load_runtime_config(store):
2007
+ data_dir = _store_data_dir(store)
2008
+ doc = {}
2009
+ if data_dir is not None:
2010
+ doc = _load_json(data_dir / CORE_CONFIG_FILE, {})
2011
+ if not isinstance(doc, dict) or not doc:
2012
+ doc = _RUNTIME_CONFIG
2013
+ return doc if isinstance(doc, dict) else {}
2014
+
2015
+
2016
+ def configure(payload):
2017
+ """task-223: whole config arrives once; the core owns every decision."""
2018
+ data = payload if isinstance(payload, dict) else {}
2019
+ store = data.get("store")
2020
+ data_dir = data.get("data_dir")
2021
+ if not data_dir:
2022
+ data_dir = _store_data_dir(store) if store else None
2023
+ doc = {key: value for key, value in data.items()
2024
+ if key not in ("store", "data_dir")}
2025
+ doc["updated_at"] = round(time.time(), 3)
2026
+ if data_dir is not None:
2027
+ try:
2028
+ _atomic_write(Path(data_dir) / CORE_CONFIG_FILE, doc)
2029
+ except Exception:
2030
+ pass
2031
+ _RUNTIME_CONFIG.clear()
2032
+ _RUNTIME_CONFIG.update(doc)
2033
+ return {"ok": True, "configured": True,
2034
+ "switches": doc.get("switches") if isinstance(
2035
+ doc.get("switches"), dict) else {}}
2036
+
2037
+
2038
+ def _effective_switches(store, overrides=None):
2039
+ merged = dict(CORE_DEFAULTS)
2040
+ data_dir = _store_data_dir(store)
2041
+ if data_dir is not None:
2042
+ flags = _load_json(data_dir / "flags.json", {})
2043
+ for key, value in flags.items():
2044
+ if isinstance(value, bool):
2045
+ merged[str(key)] = value
2046
+ cfg = _load_runtime_config(store)
2047
+ switches = cfg.get("switches") if isinstance(cfg.get("switches"), dict) else {}
2048
+ for key, value in switches.items():
2049
+ if isinstance(value, bool):
2050
+ merged[str(key)] = value
2051
+ if isinstance(overrides, dict):
2052
+ for key, value in overrides.items():
2053
+ if isinstance(value, bool):
2054
+ merged[str(key)] = value
2055
+ return merged
2056
+
2057
+
2058
+ def _rerank_config_from(raw):
2059
+ """Accept either the adapter's rerank payload or config.json's rerank block."""
2060
+ if not isinstance(raw, dict):
2061
+ return None
2062
+ if "rerank_enabled" in raw:
2063
+ return raw
2064
+ return {"rerank_enabled": raw.get("enabled") is True,
2065
+ "rerank_base_url": str(raw.get("base_url") or ""),
2066
+ "rerank_model": str(raw.get("model") or ""),
2067
+ "rerank_api_key": str(raw.get("api_key") or ""),
2068
+ "rerank_top_n": int(raw.get("top_n") or 20),
2069
+ "rerank_timeout": 8}
2070
+
2071
+
2072
+ def _recall_mode(store, requested=None):
2073
+ mode = str(requested or "").strip().lower()
2074
+ if mode in INJECT_MODES:
2075
+ return mode
2076
+ cfg = _load_runtime_config(store)
2077
+ mode = str(cfg.get("recall_mode") or "").strip().lower()
2078
+ return mode if mode in INJECT_MODES else "query"
2079
+
2080
+
2081
+ MOTIVATIONS = {
2082
+ "hold": "想先听着,不急着推进",
2083
+ "approach": "想靠近一点,再问一句",
2084
+ "withdraw": "想退半步,留点空间",
2085
+ "probe": "想探一探,但还不确定",
2086
+ "repair": "想把话圆回来",
2087
+ "rest": "有点累,想安静待着",
2088
+ "share": "想说说自己这边的事",
2089
+ }
2090
+
2091
+
2092
+ def _ui_state_body(store, session_id, data):
2093
+ """task-223: the /state payload is assembled by the core, JS forwards it."""
2094
+ entries = _visible(load_memory(store, session_id).get("entries"))
2095
+ stored_state = _load_session_state(store, session_id)
2096
+ core_path = _core_state_path(data)
2097
+ core_state_ts = 0.0
2098
+ if _core_state is not None and _core_state.exists(core_path):
2099
+ doc = _core_state.load(core_path)
2100
+ core = _core_state.core_of(doc)
2101
+ intero = doc.get("intero")
2102
+ if not isinstance(intero, dict):
2103
+ intero = compute_intero({"core": core})
2104
+ core["intero"] = intero
2105
+ core_state_ts = doc.get("ts", 0.0)
2106
+ history = list(doc.get("history") or [])[-120:]
2107
+ else:
2108
+ merged_state = apply_intero_to_state(stored_state, data.get("state"))
2109
+ intero = merged_state.get("intero") or {}
2110
+ core = dict(merged_state.get("core") or {})
2111
+ core["intero"] = intero
2112
+ history = list(stored_state.get("history") or [])[-120:]
2113
+ relationship = (relationship_read(store)["relationship"]
2114
+ if _store_root(store) is not None else {})
2115
+ recent = []
2116
+ for entry in sorted(entries, key=lambda item: _num(item.get("ts"), 0.0),
2117
+ reverse=True)[:10]:
2118
+ text = clean_text(entry.get("text"), 500)
2119
+ if not text:
2120
+ continue
2121
+ recent.append({"ts": round(_num(entry.get("ts"), 0.0) * 1000, 0),
2122
+ "text": text[:80],
2123
+ "source": str(entry.get("memory_source") or "direct"),
2124
+ "kind": str(entry.get("kind") or "")})
2125
+ rel_metrics = {"closeness": round(_num(relationship.get("closeness"), 0.5), 2),
2126
+ "trust": round(_num(relationship.get("trust"), 0.5), 2),
2127
+ "conflict": round(_num(relationship.get("conflict"), 0.0), 2),
2128
+ "bids": round(_num(relationship.get("bids"), 0.0), 2)}
2129
+ body = {"self_esteem": round(_num(core.get("self_esteem"), 0.0), 2),
2130
+ "social_pain": round(_num(core.get("social_pain"), 0.0), 2),
2131
+ "energy": round(_num(core.get("energy"), 0.5), 2),
2132
+ "social": round(_num(core.get("social"), 0.5), 2)}
2133
+ trace_rows = trace_read(store, session_id, 1)
2134
+ last_trace = trace_rows[-1] if trace_rows else {}
2135
+ last_turn = last_trace.get("intent") if isinstance(last_trace, dict) else {}
2136
+ intent = str((last_turn or {}).get("kind") or "")[:24]
2137
+ governance_summary = None
2138
+ if _governance is not None:
2139
+ try:
2140
+ governance_summary = _governance.summary(store)
2141
+ except Exception:
2142
+ governance_summary = None
2143
+ relationship_summary = None
2144
+ if _relationship_state is not None:
2145
+ try:
2146
+ relationship_summary = _relationship_state.summary(store, relationship)
2147
+ except Exception:
2148
+ relationship_summary = None
2149
+ identity_summary = None
2150
+ if _self_layer is not None:
2151
+ try:
2152
+ identity_summary = _self_layer.summary(store)
2153
+ except Exception:
2154
+ identity_summary = None
2155
+ autonomy_view = None
2156
+ if _autonomy is not None:
2157
+ try:
2158
+ switches = _effective_switches(store)
2159
+ metrics = _autonomy.metrics(store)
2160
+ autonomy_view = {
2161
+ "enabled": bool(switches.get("autonomy", True)),
2162
+ "actions_today": metrics["actions_today"],
2163
+ "sends_today": metrics["sends_today"],
2164
+ "actions_hour": metrics["actions_hour"],
2165
+ "sends_hour": metrics["sends_hour"],
2166
+ "limits": _autonomy.limits(store)}
2167
+ except Exception:
2168
+ autonomy_view = None
2169
+ identity = {"v": round(_num(core.get("v"), 0.0), 2),
2170
+ "intero": intero,
2171
+ "governance": governance_summary,
2172
+ "relationship_summary": relationship_summary,
2173
+ "identity": identity_summary,
2174
+ "available": True,
2175
+ "session_id": sanitize_session_id(session_id),
2176
+ "memory_count": len(entries),
2177
+ "memory": {"count": len(entries)},
2178
+ "memory_recent": recent,
2179
+ "relationship_metrics": rel_metrics,
2180
+ "body_metrics": body,
2181
+ "narrative": {"intent": intent, "length_factor": 1.0,
2182
+ "initiative": 0.5, "metacog_band": "",
2183
+ "allow_correction": False},
2184
+ "motivation": MOTIVATIONS.get(intent, "安静待着,等你开口"),
2185
+ "tone": {"label": ""},
2186
+ "relation": {"stage": ""},
2187
+ "autonomy": {"state": "静", **((autonomy_view or {}))},
2188
+ "history": history,
2189
+ "core_state_ts": core_state_ts,
2190
+ "recall_count": _recall_count(store, session_id),
2191
+ "governance_numbers": (governance_summary or {}),
2192
+ "proactive_sent_today": _proactive_sent_today(store),
2193
+ "core": core,
2194
+ "source": "core"}
2195
+ return identity
2196
+
2197
+
2198
+ def _embed_stats_snapshot():
2199
+ """Embedding counters: persisted file (spawn-per-call) wins over process."""
2200
+ data = {}
2201
+ try:
2202
+ from biocore_memory.recall.embedding import embed_stats as _embed_stats
2203
+ data = dict(_embed_stats())
2204
+ except Exception:
2205
+ data = {}
2206
+ path = str(os.environ.get("BIOCORE_EMBED_STATS_FILE") or "").strip()
2207
+ if path and os.path.isfile(path):
2208
+ persisted = _load_json(Path(path), {})
2209
+ if isinstance(persisted, dict) and persisted:
2210
+ data = {key: int(persisted.get(key, data.get(key, 0)) or 0)
2211
+ for key in ("hit", "disk_hit", "miss", "remote",
2212
+ "local", "fail")}
2213
+ return data
2214
+
2215
+
1561
2216
  # ---------------------------------------------------------------- turn trace
1562
2217
  TRACE_DIR = "traces"
1563
2218
  TRACE_KEEP = 50
@@ -1773,11 +2428,52 @@ def dispatch(action, payload=None):
1773
2428
  if "user_text" not in data:
1774
2429
  return bad("missing user_text")
1775
2430
  user_text = clean_text(data.get("user_text"))
2431
+ switches = _effective_switches(store, data.get("switches"))
2432
+ data["switches"] = switches
2433
+ if not switches.get("core_enabled", True):
2434
+ return {"ok": True, "prompt_block": "", "state": {},
2435
+ "disabled": True, "reason": "core_disabled"}
1776
2436
  merge_shared_relationship_into_state(session_id, data)
1777
2437
  session_state = apply_persona_to_session(session_id, data)
1778
2438
  session_state = session_state if isinstance(session_state, dict) else {}
1779
- session_state = apply_intero_to_state(session_state, data.get("state"))
1780
2439
  state_store = _relationship_store(data)
2440
+ core_path = _core_state_path(data)
2441
+ if _core_state is not None and core_path is not None:
2442
+ payload_state = (data.get("state")
2443
+ if isinstance(data.get("state"), dict) else {})
2444
+ payload_core = (payload_state.get("core")
2445
+ if isinstance(payload_state.get("core"), dict)
2446
+ else {})
2447
+ legacy_core = dict(payload_core)
2448
+ session_core = session_state.get("core")
2449
+ if isinstance(session_core, dict):
2450
+ legacy_core.update(session_core)
2451
+ if _core_state.exists(core_path):
2452
+ _core_state.migrate_session(core_path, legacy_core,
2453
+ session_id, now=time.time())
2454
+ else:
2455
+ passed = {key: legacy_core[key] for key in _CORE_FIELDS
2456
+ if key in legacy_core}
2457
+ _core_state.apply(core_path, absolute=passed,
2458
+ now=time.time(), session_id=session_id,
2459
+ intero_fn=_intero_from_core)
2460
+ doc = _core_state.load(core_path)
2461
+ if doc.get("intero") is None:
2462
+ _core_state.apply(core_path, now=time.time(),
2463
+ session_id=session_id,
2464
+ intero_fn=_intero_from_core)
2465
+ doc = _core_state.load(core_path)
2466
+ core = dict(legacy_core)
2467
+ core.update(_core_state.core_of(doc))
2468
+ if isinstance(doc.get("intero"), dict):
2469
+ core["intero"] = dict(doc["intero"])
2470
+ session_state["intero"] = dict(doc["intero"])
2471
+ session_state["core"] = core
2472
+ session_state["core_delta"] = {}
2473
+ session_state["core_state_ts"] = doc.get("ts", 0.0)
2474
+ else:
2475
+ session_state = apply_intero_to_state(session_state,
2476
+ data.get("state"))
1781
2477
  if state_store is not None:
1782
2478
  _save_session_state(state_store, session_id, session_state)
1783
2479
  capture_enabled = _as_bool(data.get("capture"), True)
@@ -1818,6 +2514,15 @@ def dispatch(action, payload=None):
1818
2514
  return bad("missing session_id")
1819
2515
  if "assistant_text" not in data:
1820
2516
  return bad("missing assistant_text")
2517
+ switches = _effective_switches(store, data.get("switches"))
2518
+ data["switches"] = switches
2519
+ if not switches.get("core_enabled", True):
2520
+ return {"ok": True, "gate": {"blocked": False, "action": "pass",
2521
+ "text": str(data.get("assistant_text") or ""),
2522
+ "rule": "", "label": ""},
2523
+ "captured": None, "clean_text": str(data.get("assistant_text") or ""),
2524
+ "trace": None, "trace_file": "", "disabled": True,
2525
+ "reason": "core_disabled"}
1821
2526
  raw_assistant = clean_text(data.get("assistant_text"))
1822
2527
  do_sanitize = _as_bool(data.get("sanitize_reply"), True)
1823
2528
  cleaned = raw_assistant
@@ -1864,6 +2569,50 @@ def dispatch(action, payload=None):
1864
2569
  "reason": "disabled" if not capture_enabled else "no_store",
1865
2570
  }, "entry": None}
1866
2571
  merge_state_relationship_into_shared(session_id, data)
2572
+ core_snapshot = None
2573
+ core_path = _core_state_path(data)
2574
+ if _core_state is not None and core_path is not None:
2575
+ deltas = _state_deltas(data)
2576
+ session_doc = (_load_session_state(store, session_id)
2577
+ if store is not None else {})
2578
+ legacy_core = session_doc.get("core")
2579
+ legacy_core = legacy_core if isinstance(legacy_core, dict) else {}
2580
+ if not _core_state.exists(core_path):
2581
+ passed = {key: legacy_core[key] for key in _CORE_FIELDS
2582
+ if key in legacy_core}
2583
+ _core_state.apply(core_path, absolute=passed,
2584
+ now=time.time(), session_id=session_id,
2585
+ intero_fn=_intero_from_core)
2586
+ shared_rel = (relationship_read(store)["relationship"]
2587
+ if store is not None else {})
2588
+ fixed = {key: shared_rel[key]
2589
+ for key in ("trust", "closeness", "conflict")
2590
+ if isinstance(shared_rel.get(key), (int, float))}
2591
+ doc = _core_state.apply(core_path, deltas=deltas,
2592
+ absolute=fixed, now=time.time(),
2593
+ session_id=session_id,
2594
+ intero_fn=_intero_from_core)
2595
+ if doc:
2596
+ core = dict(legacy_core)
2597
+ core.update(_core_state.core_of(doc))
2598
+ if isinstance(doc.get("intero"), dict):
2599
+ core["intero"] = dict(doc["intero"])
2600
+ session_doc["intero"] = dict(doc["intero"])
2601
+ session_doc["core"] = core
2602
+ session_doc["core_delta"] = dict(deltas)
2603
+ session_doc["core_state_ts"] = doc.get("ts", 0.0)
2604
+ if store is not None:
2605
+ _save_session_state(store, session_id, session_doc)
2606
+ core_snapshot = _core_state.snapshot(core_path)
2607
+ governance_info = _governance_tick(store, now=time.time())
2608
+ relationship_info = _relationship_tick(
2609
+ store, data.get("user_text"), now=time.time())
2610
+ identity_info = None
2611
+ if relationship_info:
2612
+ identity_info = _identity_tick(
2613
+ store, narrative={"kind": relationship_info.get("kind"),
2614
+ "text": str(data.get("user_text") or "")[:60]},
2615
+ now=time.time())
1867
2616
  decision = captured.get("decision") or {}
1868
2617
  entry = captured.get("entry") or {}
1869
2618
  trace = build_trace(session_id, data, gate, {"entry_id": entry.get("id")})
@@ -1881,27 +2630,93 @@ def dispatch(action, payload=None):
1881
2630
  "removed_tags": int(removed_tags),
1882
2631
  "trace": trace,
1883
2632
  "trace_file": trace_file,
2633
+ "core_state": core_snapshot,
2634
+ "governance": governance_info,
2635
+ "relationship": relationship_info,
2636
+ "identity": identity_info,
1884
2637
  }
2638
+ if action == "stats":
2639
+ switches = _effective_switches(store, data.get("switches"))
2640
+ if not switches.get("core_enabled", True):
2641
+ return {"ok": True, "disabled": True, "reason": "core_disabled",
2642
+ "embed": {key: 0 for key in ("hit", "disk_hit", "miss",
2643
+ "remote", "local", "fail")},
2644
+ "switches": {}, "store": str(store)}
2645
+ embed = _embed_stats_snapshot()
2646
+ names = ("mood_recall", "affect_encoding", "memory_feedback",
2647
+ "preference_drift")
2648
+ out = {name: {"enabled": bool(switches.get(name)), "source": "core"}
2649
+ for name in names}
2650
+ out["core_enabled"] = {"enabled": True, "source": "core"}
2651
+ governance_info = None
2652
+ if _governance is not None:
2653
+ try:
2654
+ governance_info = _governance.summary(store)
2655
+ except Exception:
2656
+ governance_info = None
2657
+ relationship_info = None
2658
+ if _relationship_state is not None:
2659
+ try:
2660
+ relationship_info = _relationship_state.summary(
2661
+ store, relationship_read(store)["relationship"])
2662
+ except Exception:
2663
+ relationship_info = None
2664
+ identity_info = None
2665
+ if _self_layer is not None:
2666
+ try:
2667
+ identity_info = _self_layer.summary(store)
2668
+ except Exception:
2669
+ identity_info = None
2670
+ autonomy_info = None
2671
+ if _autonomy is not None:
2672
+ try:
2673
+ metrics = _autonomy.metrics(store)
2674
+ autonomy_info = {"enabled": bool(switches.get("autonomy", True)),
2675
+ "actions_today": metrics["actions_today"],
2676
+ "sends_today": metrics["sends_today"],
2677
+ "limits": _autonomy.limits(store)}
2678
+ except Exception:
2679
+ autonomy_info = None
2680
+ return {"ok": True, "embed": embed, "switches": out,
2681
+ "governance": governance_info,
2682
+ "relationship": relationship_info,
2683
+ "identity": identity_info,
2684
+ "autonomy": autonomy_info,
2685
+ "store": str(store)}
2686
+ if action == "autonomy_tick":
2687
+ return _autonomy_tick(store, data) or {
2688
+ "ok": False, "reason": "autonomy_unavailable"}
1885
2689
  if action == "state":
1886
2690
  session_id = str(data.get("session_id") or "").strip()
1887
2691
  if not session_id:
1888
2692
  return bad("missing session_id")
1889
- entries = _visible(load_memory(store, session_id).get("entries"))
1890
- state_payload = {"session_id": sanitize_session_id(session_id),
1891
- "memory": {"count": len(entries)}}
2693
+ switches = _effective_switches(store, data.get("switches"))
2694
+ if not switches.get("core_enabled", True):
2695
+ return {"ok": True, "state": {"available": False,
2696
+ "reason": "core_disabled",
2697
+ "session_id": sanitize_session_id(session_id)}}
2698
+ body = _ui_state_body(store, session_id, data)
1892
2699
  stored_state = _load_session_state(store, session_id)
1893
- merged_state = apply_intero_to_state(stored_state, data.get("state"))
1894
- intero = merged_state["intero"]
1895
- state_payload["intero"] = intero
1896
- if isinstance(merged_state.get("core"), dict):
1897
- state_payload["core"] = merged_state["core"]
1898
2700
  if stored_state.get("persona_version"):
1899
- state_payload["persona_version"] = str(
2701
+ body["persona_version"] = str(
1900
2702
  stored_state.get("persona_version") or "")
1901
2703
  if _store_root(store) is not None:
1902
- state_payload["relationship"] = relationship_read(store)["relationship"]
1903
- return {"ok": True, "state": state_payload,
1904
- "ui_payload": {"intero": intero}}
2704
+ body["relationship"] = relationship_read(store)["relationship"]
2705
+ body["switches"] = {key: bool(switches.get(key)) for key in
2706
+ ("core_enabled", "mood_recall",
2707
+ "affect_encoding", "memory_feedback",
2708
+ "preference_drift")}
2709
+ return {"ok": True, "state": body,
2710
+ "ui_payload": {"intero": body.get("intero") or {}}}
2711
+ if action == "series":
2712
+ switches = _effective_switches(store, data.get("switches"))
2713
+ if not switches.get("core_enabled", True):
2714
+ return {"ok": True, "series": {key: [] for key in
2715
+ ("ts", "v", "tension", "warmth",
2716
+ "heaviness", "clarity", "trust",
2717
+ "closeness")},
2718
+ "points": [], "source": "disabled", "disabled": True}
2719
+ return _series_payload(store, data)
1905
2720
  if action == "switches_get":
1906
2721
  current = normalize_switches(data.get("switches"))
1907
2722
  key = str(data.get("key") or "").strip()
@@ -1980,10 +2795,22 @@ def dispatch(action, payload=None):
1980
2795
  store, now=data.get("now"), state_file=data.get("state_file"))
1981
2796
  if action in ("memory_inject", "memory"):
1982
2797
  session_id = data.get("session_id") or data.get("session") or "default"
1983
- result = inject_block(session_id, store,
1984
- max_chars=data.get("max_chars", MAX_CHARS),
1985
- top_k=data.get("top_k", TOP_K))
2798
+ switches = _effective_switches(store, data.get("switches"))
2799
+ if not switches.get("core_enabled", True):
2800
+ return {"ok": True, "text": "", "chars": 0, "ids": [],
2801
+ "disabled": True, "reason": "core_disabled"}
2802
+ result = inject_block(
2803
+ session_id, store,
2804
+ max_chars=data.get("max_chars", MAX_CHARS),
2805
+ top_k=data.get("top_k", TOP_K),
2806
+ query=data.get("query") or data.get("user_text") or "",
2807
+ mode=_recall_mode(store, data.get("mode")),
2808
+ rerank_config=(data.get("rerank_config")
2809
+ or _rerank_config_from(
2810
+ _load_runtime_config(store).get("rerank"))))
1986
2811
  return {"ok": True, **result}
2812
+ if action == "configure":
2813
+ return configure(data)
1987
2814
  return {"ok": False, "reason": "unknown_action", "action": action}
1988
2815
  except Exception as exc: # CLI must always answer one JSON line
1989
2816
  return {"ok": False, "reason": "pipeline_error",