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,472 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Standby initiative mode: when idle, let a small model write one line.
3
+
4
+ Config (dict or BIOCORE_STANDBY_* env; every key optional):
5
+ standby_enabled False
6
+ standby_model_base_url ""
7
+ standby_model ""
8
+ standby_model_key ""
9
+ standby_every_minutes 30
10
+ standby_active_hours "09:00-23:00"
11
+ standby_max_per_day 6
12
+ standby_channel "outbox"
13
+ standby_timeout 20
14
+ standby_store "" (memory store root; defaults to plugin data/memory)
15
+ standby_session_id "standby"
16
+
17
+ tick(now) reads the same session state as turn_start, asks the existing
18
+ pipeline.proactive_tick for the decision, then (only if should_send) builds a
19
+ full-persona prompt and calls an OpenAI-compatible /v1/chat/completions.
20
+ A successful line is sanitized and appended to
21
+ <store>/proactive/outbox.jsonl; cooldown/daily bookkeeping goes to
22
+ <store>/proactive/state.json. Any model failure writes no outbox.
23
+
24
+ CLI: python biocore_runtime/standby.py tick
25
+ python biocore_runtime/standby.py sleep-loop [--once]
26
+ """
27
+ from __future__ import annotations
28
+
29
+ import json
30
+ import math
31
+ import os
32
+ import sys
33
+ import time
34
+ import urllib.error
35
+ import urllib.request
36
+ from pathlib import Path
37
+
38
+ _PLUGIN_ROOT = str(Path(__file__).resolve().parents[1])
39
+ if _PLUGIN_ROOT not in sys.path:
40
+ sys.path.insert(0, _PLUGIN_ROOT)
41
+
42
+ from biocore_runtime import pipeline as _pipeline # noqa: E402
43
+
44
+ try:
45
+ from biocore_model import sanitize as _sanitize
46
+ except Exception: # pragma: no cover
47
+ _sanitize = None
48
+
49
+ DEFAULTS = {
50
+ "standby_enabled": False,
51
+ "standby_model_base_url": "",
52
+ "standby_model": "",
53
+ "standby_model_key": "",
54
+ "standby_every_minutes": 30,
55
+ "standby_active_hours": "09:00-23:00",
56
+ "standby_max_per_day": 6,
57
+ "standby_channel": "outbox",
58
+ "standby_timeout": 20.0,
59
+ "standby_store": "",
60
+ "standby_session_id": "standby",
61
+ "standby_min_trust": 0.6,
62
+ "standby_require_open_loop": True,
63
+ }
64
+
65
+
66
+ def _num(value, default=0.0):
67
+ try:
68
+ number = float(value)
69
+ except Exception:
70
+ return float(default)
71
+ return number if math.isfinite(number) else float(default)
72
+
73
+
74
+ def _as_bool(value, default=False):
75
+ if isinstance(value, bool):
76
+ return value
77
+ if isinstance(value, (int, float)):
78
+ return bool(value)
79
+ if isinstance(value, str):
80
+ lowered = value.strip().lower()
81
+ if lowered in ("1", "true", "yes", "on"):
82
+ return True
83
+ if lowered in ("0", "false", "no", "off", ""):
84
+ return False
85
+ return bool(default)
86
+
87
+
88
+ def normalize_config(config=None):
89
+ data = config if isinstance(config, dict) else {}
90
+ out = dict(DEFAULTS)
91
+ for key in DEFAULTS:
92
+ env_key = "BIOCORE_" + key.upper()
93
+ if key in data:
94
+ out[key] = data[key]
95
+ elif os.environ.get(env_key) is not None:
96
+ out[key] = os.environ.get(env_key)
97
+ out["standby_enabled"] = _as_bool(out["standby_enabled"], False)
98
+ for key in ("standby_model_base_url", "standby_model",
99
+ "standby_model_key", "standby_channel", "standby_store",
100
+ "standby_session_id"):
101
+ out[key] = str(out.get(key) or "").strip()
102
+ out["standby_every_minutes"] = max(
103
+ 1, min(24 * 60, int(_num(out.get("standby_every_minutes"), 30))))
104
+ out["standby_max_per_day"] = max(
105
+ 1, min(100, int(_num(out.get("standby_max_per_day"), 6))))
106
+ out["standby_timeout"] = max(
107
+ 0.2, min(120.0, _num(out.get("standby_timeout"), 20.0)))
108
+ out["standby_min_trust"] = max(
109
+ 0.0, min(1.0, _num(out.get("standby_min_trust"), 0.6)))
110
+ out["standby_require_open_loop"] = _as_bool(
111
+ out.get("standby_require_open_loop"), True)
112
+ out["standby_session_id"] = out["standby_session_id"] or "standby"
113
+ return out
114
+
115
+
116
+ def _store_root(store):
117
+ raw = str(store or "").strip()
118
+ if raw:
119
+ return Path(raw)
120
+ return Path(_pipeline._default_store())
121
+
122
+
123
+ def _proactive_dir(store):
124
+ return _store_root(store) / "proactive"
125
+
126
+
127
+ def _outbox_path(store):
128
+ return _proactive_dir(store) / "outbox.jsonl"
129
+
130
+
131
+ def _bookkeeping_path(store):
132
+ return _proactive_dir(store) / "state.json"
133
+
134
+
135
+ def _load_json(path, fallback):
136
+ try:
137
+ if path is None or not Path(path).exists():
138
+ return fallback
139
+ data = json.loads(Path(path).read_text(encoding="utf-8"))
140
+ return data if isinstance(data, dict) else fallback
141
+ except Exception:
142
+ return fallback
143
+
144
+
145
+ def _write_json(path, data):
146
+ path = Path(path)
147
+ path.parent.mkdir(parents=True, exist_ok=True)
148
+ tmp = path.with_name(path.name + ".tmp")
149
+ tmp.write_text(json.dumps(data, ensure_ascii=False, indent=2),
150
+ encoding="utf-8")
151
+ os.replace(tmp, path)
152
+
153
+
154
+ def _in_active_hours(now, spec):
155
+ text = str(spec or "").strip()
156
+ if not text:
157
+ return True
158
+ try:
159
+ start_text, end_text = text.split("-", 1)
160
+ start_h, start_m = [int(part) for part in start_text.split(":")]
161
+ end_h, end_m = [int(part) for part in end_text.split(":")]
162
+ except Exception:
163
+ return True
164
+ local = time.localtime(now)
165
+ minutes = local.tm_hour * 60 + local.tm_min
166
+ start = start_h * 60 + start_m
167
+ end = end_h * 60 + end_m
168
+ if start == end:
169
+ return True
170
+ if start < end:
171
+ return start <= minutes < end
172
+ return minutes >= start or minutes < end
173
+
174
+
175
+ def _day_key(now):
176
+ return time.strftime("%Y-%m-%d", time.localtime(now))
177
+
178
+
179
+ def _sent_today(state, now):
180
+ sent = state.get("sent") if isinstance(state.get("sent"), list) else []
181
+ today = _day_key(now)
182
+ values = []
183
+ for item in sent:
184
+ stamp = _num(item, 0.0)
185
+ if stamp > 0 and time.strftime("%Y-%m-%d", time.localtime(stamp)) == today:
186
+ values.append(stamp)
187
+ return values
188
+
189
+
190
+ def load_state(store):
191
+ return _load_json(_bookkeeping_path(store), {})
192
+
193
+
194
+ def load_session_state(store, session_id):
195
+ state = _pipeline._load_session_state(_store_root(store), session_id)
196
+ if not isinstance(state, dict):
197
+ state = {}
198
+ return _pipeline.apply_intero_to_state(state)
199
+
200
+
201
+ def build_prompt(state, memories, persona_text="", channel="outbox"):
202
+ """Full persona (never truncated) + thinking rule + state + recent memories."""
203
+ parts = []
204
+ if persona_text:
205
+ parts.append(str(persona_text))
206
+ parts.append(_pipeline.THINKING_STYLE_TEXT)
207
+ try:
208
+ state_line = _pipeline._state_block(state, 200)
209
+ except Exception:
210
+ state_line = "此刻:心情平稳。"
211
+ parts.append(state_line)
212
+ recent = [str(item).strip() for item in (memories or []) if str(item).strip()]
213
+ if recent:
214
+ parts.append("最近记得:" + ";".join(recent[:3]))
215
+ prompt = "\n\n".join(parts)
216
+ return prompt
217
+
218
+
219
+ def _recent_memories(store, session_id, limit=3):
220
+ try:
221
+ doc = _pipeline.load_memory(_store_root(store), session_id)
222
+ entries = [entry for entry in (doc.get("entries") or [])
223
+ if isinstance(entry, dict) and not entry.get("deleted")
224
+ and not entry.get("archived")]
225
+ entries.sort(key=lambda entry: _num(entry.get("ts"), 0.0), reverse=True)
226
+ return [str(entry.get("text") or "")[:120] for entry in entries[:limit]]
227
+ except Exception:
228
+ return []
229
+
230
+
231
+ def _persona_text(store):
232
+ try:
233
+ profile = _pipeline.load_persona_profile({"store": str(_store_root(store))})
234
+ return str((profile or {}).get("persona_text") or "")
235
+ except Exception:
236
+ return ""
237
+
238
+
239
+ def _chat_url(base_url):
240
+ base = str(base_url or "").strip().rstrip("/")
241
+ if not base:
242
+ return ""
243
+ if base.endswith("/v1"):
244
+ return base + "/chat/completions"
245
+ return base + "/v1/chat/completions"
246
+
247
+
248
+ def _urllib_transport(url, payload, timeout, headers):
249
+ request = urllib.request.Request(
250
+ url, data=json.dumps(payload, ensure_ascii=False).encode("utf-8"),
251
+ headers=headers, method="POST")
252
+ with urllib.request.urlopen(request, timeout=timeout) as response:
253
+ status = int(getattr(response, "status", 200) or 200)
254
+ body = response.read()
255
+ return status, body
256
+
257
+
258
+ def _extract_text(response_body):
259
+ try:
260
+ if isinstance(response_body, bytes):
261
+ response_body = response_body.decode("utf-8", "replace")
262
+ data = json.loads(response_body)
263
+ choices = data.get("choices") if isinstance(data, dict) else None
264
+ if not isinstance(choices, list) or not choices:
265
+ return ""
266
+ message = choices[0].get("message") if isinstance(choices[0], dict) else {}
267
+ return str((message or {}).get("content") or "").strip()
268
+ except Exception:
269
+ return ""
270
+
271
+
272
+ def generate(prompt, config, transport=None):
273
+ """Call the OpenAI-compatible chat endpoint; return (text, reason)."""
274
+ cfg = normalize_config(config)
275
+ base = cfg["standby_model_base_url"]
276
+ model = cfg["standby_model"]
277
+ if not base or not model:
278
+ return "", "missing_config"
279
+ url = _chat_url(base)
280
+ if not url:
281
+ return "", "missing_config"
282
+ payload = {
283
+ "model": model,
284
+ "messages": [
285
+ {"role": "system", "content": prompt},
286
+ {"role": "user", "content": "现在没有人说话。你想主动发一句,只回一句话,不超过60字。"},
287
+ ],
288
+ "temperature": 0.8,
289
+ "max_tokens": 120,
290
+ }
291
+ headers = {"Content-Type": "application/json"}
292
+ if cfg["standby_model_key"]:
293
+ headers["Authorization"] = "Bearer " + cfg["standby_model_key"]
294
+ call = transport or _urllib_transport
295
+ try:
296
+ status, body = call(url, payload, cfg["standby_timeout"], headers)
297
+ except (TimeoutError, OSError) as error:
298
+ return "", "timeout" if isinstance(error, TimeoutError) else "model_error"
299
+ except Exception:
300
+ return "", "model_error"
301
+ try:
302
+ status = int(status)
303
+ except Exception:
304
+ status = 0
305
+ if status < 200 or status >= 300:
306
+ return "", "http_%s" % status
307
+ text = _extract_text(body)
308
+ if not text:
309
+ return "", "empty_reply"
310
+ text = text.replace("\n", " ").strip()
311
+ if len(text) > 60:
312
+ text = text[:59].rstrip() + "…"
313
+ if _sanitize is not None:
314
+ try:
315
+ text = _sanitize.clean(text)
316
+ except Exception:
317
+ pass
318
+ if not str(text).strip():
319
+ return "", "empty_reply"
320
+ return str(text).strip(), "ok"
321
+
322
+
323
+ def _bookkeep(store, now, state, sent=False, reason="", text=""):
324
+ path = _bookkeeping_path(store)
325
+ sent_list = [stamp for stamp in (state.get("sent") or [])
326
+ if _num(stamp, 0.0) > 0 and now - _num(stamp, 0.0) < 7 * 86400]
327
+ if sent:
328
+ sent_list.append(now)
329
+ data = {
330
+ "last_attempt_ts": now,
331
+ "last_sent_ts": now if sent else _num(state.get("last_sent_ts"), 0.0),
332
+ "sent": sent_list[-50:],
333
+ "last_reason": reason,
334
+ "last_text": text[:80],
335
+ "ts": now,
336
+ }
337
+ try:
338
+ _write_json(path, data)
339
+ except Exception:
340
+ pass
341
+ return data
342
+
343
+
344
+ def tick(now=None, config=None, transport=None, proactive=None):
345
+ """One standby tick. Returns {ok, sent, reason, text?, channel, outbox?}."""
346
+ cfg = normalize_config(config)
347
+ store = cfg["standby_store"] or _pipeline._default_store()
348
+ session_id = cfg["standby_session_id"]
349
+ now_value = _num(now, time.time())
350
+ if now_value > 1e12:
351
+ now_value = now_value / 1000.0
352
+ result = {"ok": True, "sent": False, "reason": "",
353
+ "channel": cfg["standby_channel"]}
354
+ if not cfg["standby_enabled"]:
355
+ result["reason"] = "disabled"
356
+ return result
357
+ if not _in_active_hours(now_value, cfg["standby_active_hours"]):
358
+ result["reason"] = "outside_active_hours"
359
+ return result
360
+ state = load_state(store)
361
+ if len(_sent_today(state, now_value)) >= cfg["standby_max_per_day"]:
362
+ result["reason"] = "daily_cap"
363
+ return result
364
+ last_attempt = _num(state.get("last_attempt_ts"), 0.0)
365
+ last_sent = _num(state.get("last_sent_ts"), 0.0)
366
+ cooldown_until = max(last_attempt, last_sent) + cfg["standby_every_minutes"] * 60.0
367
+ if cooldown_until > now_value:
368
+ result["reason"] = "cooldown"
369
+ return result
370
+ session_state = load_session_state(store, session_id)
371
+ last_human = _num(session_state.get("last_human_ts"),
372
+ now_value - max(cfg["standby_every_minutes"], 180) * 60.0)
373
+ switches = {
374
+ "proactiveEnabled": True,
375
+ "proactiveSend": True,
376
+ "proactiveMinGapMinutes": cfg["standby_every_minutes"],
377
+ "proactivePerHour": 6,
378
+ "proactiveMinTrust": cfg["standby_min_trust"],
379
+ "proactiveMaxChars": 60,
380
+ "proactiveRequireOpenLoop": cfg["standby_require_open_loop"],
381
+ }
382
+ decision = proactive
383
+ if decision is None:
384
+ decision = _pipeline.proactive_tick
385
+ try:
386
+ decision_result = decision(
387
+ session_id=session_id, last_human_ts=last_human,
388
+ switches=switches, store=store, now=now_value * 1000.0,
389
+ state_file=str(_pipeline.state_path(store, session_id)))
390
+ except Exception:
391
+ result["reason"] = "decision_error"
392
+ return result
393
+ if not isinstance(decision_result, dict):
394
+ result["reason"] = "decision_error"
395
+ return result
396
+ if not decision_result.get("should_send"):
397
+ result["reason"] = str(decision_result.get("reason") or "should_not_send")
398
+ return result
399
+ memories = _recent_memories(store, session_id)
400
+ persona = _persona_text(store)
401
+ prompt = build_prompt(session_state, memories, persona,
402
+ cfg["standby_channel"])
403
+ text, reason = generate(prompt, cfg, transport=transport)
404
+ if reason != "ok" or not text:
405
+ _bookkeep(store, now_value, state, sent=False, reason=reason)
406
+ result["reason"] = reason
407
+ return result
408
+ outbox = _outbox_path(store)
409
+ try:
410
+ outbox.parent.mkdir(parents=True, exist_ok=True)
411
+ row = {
412
+ "ts": round(now_value, 3),
413
+ "text": text,
414
+ "reason": "ok",
415
+ "state_snapshot": session_state,
416
+ "channel": cfg["standby_channel"],
417
+ "model": cfg["standby_model"],
418
+ }
419
+ with outbox.open("a", encoding="utf-8") as handle:
420
+ handle.write(json.dumps(row, ensure_ascii=False) + "\n")
421
+ except Exception:
422
+ _bookkeep(store, now_value, state, sent=False, reason="outbox_write_failed")
423
+ result["reason"] = "outbox_write_failed"
424
+ return result
425
+ data = _bookkeep(store, now_value, state, sent=True, reason="ok", text=text)
426
+ result.update({"sent": True, "reason": "ok", "text": text,
427
+ "outbox": str(outbox), "sent_today": len(_sent_today(data, now_value))})
428
+ return result
429
+
430
+
431
+ def sleep_loop(config=None, once=False):
432
+ cfg = normalize_config(config)
433
+ interval = max(60, int(cfg["standby_every_minutes"] * 60))
434
+ while True:
435
+ try:
436
+ result = tick(config=cfg)
437
+ print(json.dumps(result, ensure_ascii=False))
438
+ except Exception as error:
439
+ print(json.dumps({"ok": False, "reason": "tick_error",
440
+ "detail": str(error)[:200]}, ensure_ascii=False))
441
+ if once:
442
+ return 0
443
+ try:
444
+ time.sleep(interval)
445
+ except KeyboardInterrupt:
446
+ return 0
447
+
448
+
449
+ def main(argv=None):
450
+ args = list(argv if argv is not None else sys.argv[1:])
451
+ action = args[0] if args else "tick"
452
+ config = {}
453
+ index = 1
454
+ while index < len(args):
455
+ if args[index] == "--once":
456
+ config["once"] = True
457
+ index += 1
458
+ else:
459
+ break
460
+ if action == "tick":
461
+ result = tick(config=config)
462
+ print(json.dumps(result, ensure_ascii=False))
463
+ return 0 if result.get("ok") else 1
464
+ if action == "sleep-loop":
465
+ return sleep_loop(config=config, once=bool(config.get("once")))
466
+ print(json.dumps({"ok": False, "reason": "unknown_action",
467
+ "action": action}, ensure_ascii=False))
468
+ return 1
469
+
470
+
471
+ if __name__ == "__main__":
472
+ raise SystemExit(main())
@@ -1,27 +1,75 @@
1
- /** Pure layout logic for the standalone panel (task-214).
1
+ /** BioCore panel layout model (v3 rewrite, pure logic, no DOM).
2
2
  *
3
- * No DOM here so test/selftest.mjs can exercise order/hidden/restore/storage
4
- * without a browser. panel.js owns the DOM wiring; every card carries
5
- * data-card-id and the layout is persisted in localStorage.
3
+ * One source of truth for: card registry (id -> tab/label/span), tab groups,
4
+ * hide/restore, ordering and localStorage persistence. panel.js only wires DOM
5
+ * events; selftest.mjs exercises this file inside a bare vm sandbox.
6
+ *
7
+ * Classification rule (hard): a card belongs to exactly one tab, and a tab is
8
+ * either all controls (settings) or all data (dashboard/memory/relation) or all
9
+ * diagnostics (tools). Never mixed.
6
10
  */
7
11
  (function (root) {
8
12
  'use strict'
9
13
 
10
14
  var STORAGE_KEY = 'biocore-panel-layout-v2'
11
- var DEFAULT_ORDER = [
12
- 'mood', 'tension', 'warmth', 'heaviness', 'clarity', 'chart',
13
- 'memory', 'memory-summary', 'metrics',
14
- 'persona', 'model', 'switches', 'expression', 'advanced',
15
- 'runtime', 'trace',
16
- ]
15
+ var RAIL_KEY = 'biocore-panel-rail-v1'
16
+
17
17
  var TAB_ORDER = ['dashboard', 'memory', 'relation', 'settings', 'tools']
18
- var TAB_GROUPS = Object.freeze({
19
- dashboard: ['mood', 'tension', 'warmth', 'heaviness', 'clarity', 'chart'],
20
- memory: ['memory', 'memory-summary'],
21
- relation: ['metrics'],
22
- settings: ['persona', 'model', 'switches', 'expression', 'advanced'],
23
- tools: ['runtime', 'trace'],
24
- })
18
+ var TAB_LABELS = {
19
+ dashboard: '仪表盘',
20
+ memory: '记忆',
21
+ relation: '关系',
22
+ settings: '配置',
23
+ tools: '工具',
24
+ }
25
+
26
+ /** Card registry. span is the 12-column desktop width; h the desktop band. */
27
+ var CARD_META = [
28
+ { id: 'status', tab: 'dashboard', label: '此刻', span: 6, h: 208 },
29
+ { id: 'mood', tab: 'dashboard', label: '心情', span: 3, h: 208 },
30
+ { id: 'tension', tab: 'dashboard', label: '张力', span: 3, h: 208 },
31
+ { id: 'warmth', tab: 'dashboard', label: '暖', span: 3, h: 176 },
32
+ { id: 'heaviness', tab: 'dashboard', label: '沉', span: 3, h: 176 },
33
+ { id: 'clarity', tab: 'dashboard', label: '清', span: 3, h: 176 },
34
+ { id: 'channels-avg', tab: 'dashboard', label: '四通道均值', span: 3, h: 176 },
35
+ { id: 'chart', tab: 'dashboard', label: '24 小时曲线', span: 8, h: 200 },
36
+ { id: 'expression-now', tab: 'dashboard', label: '此刻的表达', span: 4, h: 200 },
37
+ { id: 'today', tab: 'dashboard', label: '今日统计', span: 12, h: 120 },
38
+
39
+ { id: 'memory', tab: 'memory', label: '最近记忆', span: 8, h: 232 },
40
+ { id: 'memory-summary', tab: 'memory', label: '记忆概览', span: 4, h: 232 },
41
+
42
+ { id: 'metrics', tab: 'relation', label: '关系数值', span: 6, h: 176 },
43
+ { id: 'narrative', tab: 'relation', label: '叙事摘要', span: 6, h: 176 },
44
+
45
+ { id: 'persona', tab: 'settings', label: '人设', span: 6, h: 320 },
46
+ { id: 'model', tab: 'settings', label: '模型与接口', span: 6, h: 320 },
47
+ { id: 'switches', tab: 'settings', label: '开关', span: 4, h: 320 },
48
+ { id: 'expression', tab: 'settings', label: '表达', span: 4, h: 320 },
49
+ { id: 'advanced', tab: 'settings', label: '维护', span: 4, h: 320 },
50
+
51
+ { id: 'trace', tab: 'tools', label: '这一轮为什么', span: 12, h: 280 },
52
+ { id: 'runtime', tab: 'tools', label: '运行时', span: 4, h: 200 },
53
+ { id: 'diagnostics', tab: 'tools', label: '缓存与诊断', span: 8, h: 200 },
54
+ ]
55
+
56
+ var DEFAULT_ORDER = CARD_META.map(function (card) { return card.id })
57
+
58
+ var META_BY_ID = {}
59
+ for (var i = 0; i < CARD_META.length; i += 1) META_BY_ID[CARD_META[i].id] = CARD_META[i]
60
+
61
+ var TAB_GROUPS = {}
62
+ for (var t = 0; t < TAB_ORDER.length; t += 1) TAB_GROUPS[TAB_ORDER[t]] = []
63
+ for (var m = 0; m < CARD_META.length; m += 1) TAB_GROUPS[CARD_META[m].tab].push(CARD_META[m].id)
64
+
65
+ function cardMeta(id) {
66
+ return META_BY_ID[id] || null
67
+ }
68
+
69
+ function tabOf(id) {
70
+ var meta = META_BY_ID[id]
71
+ return meta ? meta.tab : ''
72
+ }
25
73
 
26
74
  function defaultLayout() {
27
75
  return { order: DEFAULT_ORDER.slice(), hidden: [] }
@@ -79,17 +127,11 @@
79
127
  var seen = {}
80
128
  for (var i = 0; i < ranked.length; i += 1) {
81
129
  var id = ranked[i]
82
- if (list.indexOf(id) >= 0 && !seen[id]) {
83
- seen[id] = 1
84
- out.push(id)
85
- }
130
+ if (list.indexOf(id) >= 0 && !seen[id]) { seen[id] = 1; out.push(id) }
86
131
  }
87
132
  for (var j = 0; j < list.length; j += 1) {
88
133
  var extra = list[j]
89
- if (!seen[extra]) {
90
- seen[extra] = 1
91
- out.push(extra)
92
- }
134
+ if (!seen[extra]) { seen[extra] = 1; out.push(extra) }
93
135
  }
94
136
  return out
95
137
  }
@@ -121,15 +163,6 @@
121
163
  return { order: normalized.order.slice(), hidden: hidden }
122
164
  }
123
165
 
124
- /** Pure pointer-intent: only edit mode drags, always via a long press
125
- * (the card itself is the handle — no separate ⠿ control). */
126
- function dragIntent(options) {
127
- var opts = options || {}
128
- if (!opts.editing) return { start: false, hold: false }
129
- return { start: false, hold: true }
130
- }
131
-
132
- /** Re-show one hidden card (undo of hideCard). */
133
166
  function showCard(layout, id) {
134
167
  var normalized = normalizeLayout(layout)
135
168
  if (normalized.hidden.indexOf(id) < 0) return normalized
@@ -137,7 +170,14 @@
137
170
  return { order: normalized.order.slice(), hidden: hidden }
138
171
  }
139
172
 
140
- /** Cards shown for one bottom-nav group (no data refetch, DOM filter only). */
173
+ /** Pointer intent: only edit mode drags; touch needs a long press first. */
174
+ function dragIntent(options) {
175
+ var opts = options || {}
176
+ if (!opts.editing) return { start: false, hold: false }
177
+ return { start: false, hold: true }
178
+ }
179
+
180
+ /** Cards belonging to one tab, in the given order. */
141
181
  function cardsForTab(ids, tab) {
142
182
  var list = Array.isArray(ids) ? ids : []
143
183
  var group = TAB_GROUPS[tab] || TAB_GROUPS.dashboard
@@ -146,8 +186,7 @@
146
186
 
147
187
  function readLayout(storage) {
148
188
  try {
149
- var raw = storage && typeof storage.getItem === 'function'
150
- ? storage.getItem(STORAGE_KEY) : null
189
+ var raw = storage && typeof storage.getItem === 'function' ? storage.getItem(STORAGE_KEY) : null
151
190
  if (!raw) return defaultLayout()
152
191
  return normalizeLayout(JSON.parse(raw))
153
192
  } catch {
@@ -177,9 +216,14 @@
177
216
 
178
217
  var api = {
179
218
  STORAGE_KEY: STORAGE_KEY,
180
- DEFAULT_ORDER: DEFAULT_ORDER.slice(),
219
+ RAIL_KEY: RAIL_KEY,
181
220
  TAB_ORDER: TAB_ORDER.slice(),
221
+ TAB_LABELS: TAB_LABELS,
182
222
  TAB_GROUPS: TAB_GROUPS,
223
+ CARD_META: CARD_META.slice(),
224
+ DEFAULT_ORDER: DEFAULT_ORDER.slice(),
225
+ cardMeta: cardMeta,
226
+ tabOf: tabOf,
183
227
  cardsForTab: cardsForTab,
184
228
  defaultLayout: defaultLayout,
185
229
  normalizeOrder: normalizeOrder,
@@ -196,4 +240,4 @@
196
240
  }
197
241
  root.BioCorePanelLayout = api
198
242
  })(typeof window !== 'undefined' ? window
199
- : (typeof globalThis !== 'undefined' ? globalThis : this));
243
+ : (typeof globalThis !== 'undefined' ? globalThis : this));