dsh-biocore 0.2.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
@@ -25,6 +25,17 @@ except Exception: # pragma: no cover - top-level/standalone tests
25
25
  from embedding_mode import resolve_mode # type: ignore
26
26
 
27
27
  EMBED_CACHE_MAX = 512
28
+
29
+ # task-221: observable cache/provider counters (process-local, never raises).
30
+ EMBED_STATS = {"hit": 0, "disk_hit": 0, "miss": 0, "remote": 0, "local": 0, "fail": 0}
31
+
32
+
33
+ def embed_stats(reset=False):
34
+ data = dict(EMBED_STATS)
35
+ if reset:
36
+ for key in EMBED_STATS:
37
+ EMBED_STATS[key] = 0
38
+ return data
28
39
  EMBED_TEXT_MAX_CHARS = 2000
29
40
  DEFAULT_TIMEOUT, MIN_TIMEOUT, MAX_TIMEOUT = 5.0, 0.1, 60.0
30
41
  ENV_BASE_URL = "BIOCORE_EMBEDDING_BASE_URL"
@@ -105,6 +116,104 @@ def _validate_vector(value) -> Optional[List[float]]:
105
116
  return out
106
117
 
107
118
 
119
+ CACHE_FILE_ENV = "BIOCORE_EMBEDDING_CACHE_FILE"
120
+ STATS_FILE_ENV = "BIOCORE_EMBED_STATS_FILE"
121
+ _STATS_FLUSHED = {}
122
+
123
+
124
+ def _stats_file():
125
+ return os.environ.get(STATS_FILE_ENV) or None
126
+
127
+
128
+ def _flush_stats():
129
+ """Persist counters so /stats keeps counting across spawn-per-call.
130
+
131
+ Each bridge call is a fresh process; the file accumulates deltas since the
132
+ last flush of this process (never raises, never blocks a turn).
133
+ """
134
+ path = _stats_file()
135
+ if not path:
136
+ return
137
+ try:
138
+ previous = {}
139
+ if os.path.isfile(path):
140
+ try:
141
+ with open(path, encoding="utf-8") as handle:
142
+ loaded = json.load(handle)
143
+ if isinstance(loaded, dict):
144
+ previous = loaded
145
+ except Exception:
146
+ previous = {}
147
+ merged = {}
148
+ for key in EMBED_STATS:
149
+ try:
150
+ before = int(previous.get(key, 0) or 0)
151
+ except Exception:
152
+ before = 0
153
+ merged[key] = before + EMBED_STATS[key] - _STATS_FLUSHED.get(key, 0)
154
+ directory = os.path.dirname(path)
155
+ if directory:
156
+ os.makedirs(directory, exist_ok=True)
157
+ tmp = path + ".tmp"
158
+ with open(tmp, "w", encoding="utf-8") as handle:
159
+ json.dump(merged, handle)
160
+ os.replace(tmp, path)
161
+ _STATS_FLUSHED.update(EMBED_STATS)
162
+ except Exception:
163
+ pass
164
+
165
+
166
+ def _cache_file():
167
+ path = os.environ.get(CACHE_FILE_ENV) or ""
168
+ if not path:
169
+ return None
170
+ try:
171
+ os.makedirs(path, exist_ok=True)
172
+ return os.path.join(path, "embedding-cache.jsonl")
173
+ except Exception:
174
+ return None
175
+
176
+
177
+ def _disk_lookup(file_path, key):
178
+ if not file_path or not os.path.isfile(file_path):
179
+ return None
180
+ try:
181
+ with open(file_path, encoding="utf-8") as fh:
182
+ for line in fh:
183
+ try:
184
+ row = json.loads(line)
185
+ except Exception:
186
+ continue
187
+ if row.get("k") == key:
188
+ return _validate_vector(row.get("v"))
189
+ except Exception:
190
+ return None
191
+ return None
192
+
193
+
194
+ def _disk_put(file_path, key, vector, keep=512):
195
+ if not file_path:
196
+ return
197
+ try:
198
+ rows = []
199
+ if os.path.isfile(file_path):
200
+ with open(file_path, encoding="utf-8") as fh:
201
+ for line in fh:
202
+ try:
203
+ rows.append(json.loads(line))
204
+ except Exception:
205
+ continue
206
+ rows = [r for r in rows if r.get("k") != key]
207
+ rows.append({"k": key, "v": list(vector)})
208
+ rows = rows[-keep:]
209
+ with open(file_path + ".tmp", "w", encoding="utf-8") as fh:
210
+ for row in rows:
211
+ fh.write(json.dumps(row, ensure_ascii=False) + "\n")
212
+ os.replace(file_path + ".tmp", file_path)
213
+ except Exception:
214
+ return
215
+
216
+
108
217
  def _cache_get(cache, key):
109
218
  if cache is None:
110
219
  return None
@@ -719,6 +828,13 @@ def _call_provider(provider, text, timeout):
719
828
 
720
829
  def embed(text, provider, cache=None, timeout=None):
721
830
  """Return a vector for text, or None (never raises); LRU cache optional."""
831
+ try:
832
+ return _embed_impl(text, provider, cache, timeout)
833
+ finally:
834
+ _flush_stats()
835
+
836
+
837
+ def _embed_impl(text, provider, cache=None, timeout=None):
722
838
  if provider is None:
723
839
  return None
724
840
  normalized = normalize_text(text)
@@ -727,19 +843,32 @@ def embed(text, provider, cache=None, timeout=None):
727
843
  key = cache_key(normalized)
728
844
  cached = _cache_get(cache, key)
729
845
  if cached is not None:
846
+ EMBED_STATS["hit"] += 1
730
847
  try: # true LRU when the caller passes an OrderedDict
731
848
  cache.move_to_end(key)
732
849
  except Exception:
733
850
  pass
734
851
  return cached
852
+ disk = _cache_file()
853
+ from_disk = _disk_lookup(disk, key) if disk else None
854
+ if from_disk is not None:
855
+ EMBED_STATS["disk_hit"] += 1
856
+ _cache_put(cache, key, from_disk)
857
+ return from_disk
858
+ EMBED_STATS["miss"] += 1
735
859
  checked = _validate_vector(
736
860
  _call_provider(provider, normalized, _timeout_for(provider, timeout)))
737
861
  if checked is not None:
862
+ EMBED_STATS["remote"] += 1
738
863
  _cache_put(cache, key, checked)
864
+ _disk_put(disk, key, checked)
739
865
  return checked
740
866
  # Remote failed / timed out / missing key: try the local CPU int8 model.
741
867
  local = _fallback_embed(provider, normalized)
742
868
  if local is None:
869
+ EMBED_STATS["fail"] += 1
743
870
  return None
871
+ EMBED_STATS["local"] += 1
744
872
  _cache_put(cache, key, local)
873
+ _disk_put(disk, key, local)
745
874
  return local
@@ -0,0 +1,202 @@
1
+ # -*- coding: utf-8 -*-
2
+ """task-218: recall feedback into affect ("回忆回灌情绪").
3
+
4
+ A recalled memory already carries the affect it was encoded with (task-217,
5
+ entry["affect"]). Recall may feed a small share of that stored affect back
6
+ into the live affect state, so remembering a warm moment lifts the mood a
7
+ little and remembering a bad one pulls it down -- then stops.
8
+
9
+ Three guards, all from core/config.py:
10
+
11
+ CAP per-turn |delta| <= FEEDBACK_MAX_STEP, valence and arousal apart
12
+ COOLDOWN the same entry cannot feed twice inside FEEDBACK_COOLDOWN seconds
13
+ (the caller owns the {entry_id: last_ts} map)
14
+ FLOOR at valence <= FEEDBACK_NEG_FLOOR a further negative pull is
15
+ dropped to 0 and the caller is told (blocked_negative) to run a
16
+ regulation strategy instead
17
+
18
+ Pure: no IO, no clock, no host import, never touches the store. The engine
19
+ passes the rows it actually injected (top-k), the current affect and now,
20
+ so one recall can be replayed exactly in a test.
21
+ """
22
+ from __future__ import annotations
23
+
24
+ from ..core.config import (FEEDBACK_COOLDOWN, FEEDBACK_GAIN,
25
+ FEEDBACK_MAX_STEP, FEEDBACK_NEG_FLOOR)
26
+
27
+
28
+ def _num(value, default=0.0):
29
+ try:
30
+ number = float(value)
31
+ except Exception:
32
+ return default
33
+ return number if number == number else default
34
+
35
+
36
+ def _clamp(value, low, high):
37
+ return max(low, min(high, _num(value, low)))
38
+
39
+
40
+ def _pair(source):
41
+ """(valence, arousal) from a dict, a 2-sequence or an object."""
42
+ if isinstance(source, dict):
43
+ v = source.get("v", source.get("valence", 0.0))
44
+ a = source.get("a", source.get("arousal", 0.5))
45
+ elif isinstance(source, (list, tuple)) and len(source) >= 2:
46
+ v, a = source[0], source[1]
47
+ else:
48
+ v = getattr(source, "v", getattr(source, "valence", 0.0))
49
+ a = getattr(source, "a", getattr(source, "arousal", 0.5))
50
+ return _clamp(_num(v, 0.0), -1.0, 1.0), _clamp(_num(a, 0.5), 0.0, 1.0)
51
+
52
+
53
+ def _entry_of(row):
54
+ if isinstance(row, dict):
55
+ entry = row.get("entry")
56
+ if isinstance(entry, dict):
57
+ return entry
58
+ return row if isinstance(row, dict) else {}
59
+
60
+
61
+ def stored_affect(entry):
62
+ """(valence, arousal) stored at write time, or None when absent."""
63
+ if not isinstance(entry, dict):
64
+ return None
65
+ affect = entry.get("affect")
66
+ if not isinstance(affect, dict):
67
+ return None
68
+ if "valence" not in affect and "v" not in affect:
69
+ return None
70
+ raw_v = affect.get("valence", affect.get("v"))
71
+ raw_a = affect.get("arousal", affect.get("a", 0.5))
72
+ try:
73
+ valid_v = float(raw_v)
74
+ valid_a = float(raw_a)
75
+ except Exception:
76
+ return None
77
+ if valid_v != valid_v or valid_a != valid_a: # NaN
78
+ return None
79
+ return (_clamp(valid_v, -1.0, 1.0), _clamp(valid_a, 0.0, 1.0))
80
+
81
+
82
+ def key_of(entry):
83
+ """Stable cooldown key: the entry id, else a text prefix."""
84
+ entry = entry if isinstance(entry, dict) else {}
85
+ entry_id = str(entry.get("id") or "").strip()
86
+ if entry_id:
87
+ return entry_id
88
+ return "text:" + str(entry.get("text") or "")[:40]
89
+
90
+
91
+ def _weight(entry):
92
+ return _clamp(entry.get("importance", 0.5) if isinstance(entry, dict) else 0.5,
93
+ 0.0, 1.0)
94
+
95
+
96
+ def plan(rows, current, cooldowns=None, now=0.0, budget=None,
97
+ gain=None, max_step=None, cooldown=None, neg_floor=None):
98
+ """Decide the per-turn affect nudge for one recall batch.
99
+
100
+ rows what this recall injected (row dicts or raw entries); trimmed
101
+ to budget when given
102
+ current live affect: dict {v/a | valence/arousal}, (v, a) or object
103
+ cooldowns {entry key: last feedback ts}; read only, the caller updates it
104
+ """
105
+ cooldowns = cooldowns if isinstance(cooldowns, dict) else {}
106
+ now = _num(now, 0.0)
107
+ gain = _clamp(FEEDBACK_GAIN if gain is None else gain, 0.0, 1.0)
108
+ max_step = max(0.0, _num(FEEDBACK_MAX_STEP if max_step is None else max_step,
109
+ FEEDBACK_MAX_STEP))
110
+ cool = max(0.0, _num(FEEDBACK_COOLDOWN if cooldown is None else cooldown,
111
+ FEEDBACK_COOLDOWN))
112
+ floor = _clamp(FEEDBACK_NEG_FLOOR if neg_floor is None else neg_floor,
113
+ -1.0, 1.0)
114
+ before_v, before_a = _pair(current)
115
+
116
+ batch = [row for row in (rows or []) if isinstance(row, dict)]
117
+ if budget is not None:
118
+ try:
119
+ batch = batch[:max(1, int(budget))]
120
+ except Exception:
121
+ pass
122
+
123
+ skipped = []
124
+ eligible = [] # (key, (v, a), weight)
125
+ for row in batch:
126
+ entry = _entry_of(row)
127
+ key = key_of(entry)
128
+ pair = stored_affect(entry)
129
+ if pair is None:
130
+ skipped.append({"id": key, "reason": "no_affect"})
131
+ continue
132
+ last = cooldowns.get(key)
133
+ if last is not None and now - _num(last, 0.0) < cool:
134
+ skipped.append({"id": key, "reason": "cooldown"})
135
+ continue
136
+ eligible.append((key, pair, _weight(entry)))
137
+
138
+ report = {
139
+ "enabled": True,
140
+ "before": [round(before_v, 4), round(before_a, 4)],
141
+ "target": None,
142
+ "delta": [0.0, 0.0],
143
+ "after": [round(before_v, 4), round(before_a, 4)],
144
+ "eligible": [key for key, _value, _weight_value in eligible],
145
+ "applied": [],
146
+ "skipped": skipped,
147
+ "capped": False,
148
+ "blocked_negative": False,
149
+ "regulation": None,
150
+ "reason": "",
151
+ }
152
+ if not eligible:
153
+ report["reason"] = "no_eligible_memory"
154
+ return report
155
+
156
+ total_w = sum(weight for _key, _value, weight in eligible)
157
+ if total_w <= 0:
158
+ total_w = float(len(eligible))
159
+ weights = [1.0] * len(eligible)
160
+ else:
161
+ weights = [weight for _key, _value, weight in eligible]
162
+ target_v = sum(pair[0] * weight
163
+ for (_key, pair, _weight_value), weight
164
+ in zip(eligible, weights)) / total_w
165
+ target_a = sum(pair[1] * weight
166
+ for (_key, pair, _weight_value), weight
167
+ in zip(eligible, weights)) / total_w
168
+ target_v = _clamp(target_v, -1.0, 1.0)
169
+ target_a = _clamp(target_a, 0.0, 1.0)
170
+
171
+ raw_v = (target_v - before_v) * gain
172
+ raw_a = (target_a - before_a) * gain
173
+ delta_v = max(-max_step, min(max_step, raw_v))
174
+ delta_a = max(-max_step, min(max_step, raw_a))
175
+ report["capped"] = bool(abs(raw_v) > max_step + 1e-12
176
+ or abs(raw_a) > max_step + 1e-12)
177
+
178
+ if delta_v < 0 and before_v <= floor:
179
+ # Already low: do not push further down; regulation takes over.
180
+ delta_v = 0.0
181
+ report["blocked_negative"] = True
182
+ report["reason"] = "negative_floor"
183
+
184
+ report["target"] = [round(target_v, 4), round(target_a, 4)]
185
+ report["delta"] = [round(delta_v, 6), round(delta_a, 6)]
186
+ report["after"] = [round(_clamp(before_v + delta_v, -1.0, 1.0), 4),
187
+ round(_clamp(before_a + delta_a, 0.0, 1.0), 4)]
188
+ if delta_v or delta_a:
189
+ report["applied"] = list(report["eligible"])
190
+ report["reason"] = report["reason"] or "ok"
191
+ else:
192
+ report["reason"] = report["reason"] or "no_change"
193
+ return report
194
+
195
+
196
+ def enabled(flags=None, env=None):
197
+ """Flag or env switch; both default off (old behaviour)."""
198
+ flags = flags if isinstance(flags, dict) else {}
199
+ if flags.get("memory_feedback") is True:
200
+ return True
201
+ raw = env.get("BIOCORE_MEMORY_FEEDBACK") if hasattr(env, "get") else None
202
+ return str(raw or "").strip().lower() in ("1", "true", "on", "yes")
@@ -0,0 +1,255 @@
1
+ # -*- coding: utf-8 -*-
2
+ """task-219: repeated topics slowly drift the value_order weights.
3
+
4
+ Memory already knows what keeps coming back. This module counts those topics
5
+ with the tokenizer that already ships in biocore_model (memory_store.bigrams
6
+ for CJK, a latin-word regex), maps the ones with a known dimension onto
7
+ value_order, and -- only once a topic has appeared in more than
8
+ TOPIC_THRESHOLD memory entries -- nudges that dimension by a small amount per
9
+ new occurrence.
10
+
11
+ Guards, all here so a theme can never rewrite her in one night:
12
+
13
+ THRESHOLD cumulative occurrences must pass TOPIC_THRESHOLD (5)
14
+ PER CALL one step moves a dimension by at most DAILY_CAP (0.01), which is
15
+ also the per-day allowance; callers run one step per day
16
+ BOUNDS every weight stays inside value_order.WEIGHT_FLOOR..CEIL
17
+ WATERMARK only occurrences since the previous call drift the table
18
+ (last_counts), so replaying the same batch is idempotent
19
+ DECAY a dimension whose topic stopped being active relaxes toward
20
+ value_order.DEFAULTS by DECAY_PER_DAY per elapsed day
21
+ LAYERS biocore_model only; never writes a memory entry, never touches
22
+ the persona file, never renders prompt text
23
+
24
+ Pure functions only: no IO, no clock, no host imports, no LLM calls. The
25
+ caller passes weights, counts and the last watermark, so a drift step can be
26
+ replayed exactly in a test. Default OFF: flags {"preference_drift": True} or
27
+ env BIOCORE_PREFERENCE_DRIFT=1.
28
+ """
29
+ from __future__ import annotations
30
+
31
+ import re
32
+
33
+ try:
34
+ from . import value_order as _vo
35
+ except Exception: # pragma: no cover - degraded checkout
36
+ _vo = None
37
+
38
+ TOPIC_THRESHOLD = 5
39
+ PER_REPEAT = 0.002
40
+ DAILY_CAP = 0.01
41
+ DECAY_PER_DAY = 0.0005
42
+ MAX_TOPICS = 24
43
+
44
+ _WS = re.compile(r"\s+")
45
+ _LATIN = re.compile(r"[a-z][a-z0-9_']*")
46
+ _CJK = re.compile(r"[\u4e00-\u9fff]+")
47
+
48
+ # topic keyword -> value_order dimension; the longest matching keyword wins.
49
+ TOPIC_DIMENSIONS = (
50
+ ("self_direction", ("自由", "自己做主", "自己决定", "独立", "选择", "决定")),
51
+ ("stimulation", ("新鲜", "刺激", "旅行", "冒险", "出去玩", "尝试")),
52
+ ("hedonism", ("开心", "好吃", "吃", "玩", "舒服", "放松")),
53
+ ("achievement", ("认可", "夸", "厉害", "成功", "进步", "成绩")),
54
+ ("power", ("掌控", "主导", "说话算数", "权力", "命令")),
55
+ ("security", ("安全", "害怕", "担心", "不安", "稳定")),
56
+ ("conformity", ("规矩", "礼貌", "应该", "不能", "越界")),
57
+ ("tradition", ("传统", "习惯", "以前", "老规矩")),
58
+ ("benevolence", ("陪", "需要我", "帮你", "关心", "照顾", "倾听")),
59
+ ("universalism", ("公平", "诚实", "尊重", "正义", "原则")),
60
+ )
61
+
62
+ STOPWORDS = frozenset((
63
+ "我们", "你们", "他们", "她们", "这个", "那个", "就是", "不是", "一个",
64
+ "什么", "怎么", "可以", "没有", "因为", "所以", "然后", "还是", "现在",
65
+ "今天", "明天", "知道", "觉得", "自己", "已经", "这样", "那样", "东西",
66
+ ))
67
+ FALLBACK_DEFAULTS = {
68
+ "self_direction": 0.62, "stimulation": 0.45, "hedonism": 0.40,
69
+ "achievement": 0.55, "power": 0.38, "security": 0.70,
70
+ "conformity": 0.35, "tradition": 0.30, "benevolence": 0.75,
71
+ "universalism": 0.60,
72
+ }
73
+
74
+
75
+ def _num(value, default=0.0):
76
+ try:
77
+ number = float(value)
78
+ except Exception:
79
+ return default
80
+ return number if number == number else default
81
+
82
+
83
+ def _clamp(value, low, high):
84
+ return max(low, min(high, _num(value, low)))
85
+
86
+
87
+ def _dimensions():
88
+ return tuple(_vo.DIMENSIONS) if _vo is not None else tuple(FALLBACK_DEFAULTS)
89
+
90
+
91
+ def _defaults():
92
+ return dict(_vo.DEFAULTS) if _vo is not None else dict(FALLBACK_DEFAULTS)
93
+
94
+
95
+ def enabled(flags=None, env=None):
96
+ """Flag or env switch; both default OFF (old behaviour)."""
97
+ flags = flags if isinstance(flags, dict) else {}
98
+ if flags.get("preference_drift") is True:
99
+ return True
100
+ raw = env.get("BIOCORE_PREFERENCE_DRIFT") if hasattr(env, "get") else None
101
+ return str(raw or "").strip().lower() in ("1", "true", "on", "yes")
102
+
103
+
104
+ def topics(text, limit=MAX_TOPICS):
105
+ """Topic tokens for one memory text (CJK bigrams + latin words)."""
106
+ lowered = _WS.sub(" ", str(text or "").lower())
107
+ out = []
108
+ seen = set()
109
+ for run in _CJK.findall(lowered):
110
+ if len(run) < 2:
111
+ continue
112
+ for index in range(len(run) - 1):
113
+ cand = run[index:index + 2]
114
+ if cand in seen or cand in STOPWORDS:
115
+ continue
116
+ seen.add(cand)
117
+ out.append(cand)
118
+ if len(out) >= limit:
119
+ return out
120
+ for word in _LATIN.findall(lowered):
121
+ if len(word) < 3 or word in seen or word in STOPWORDS:
122
+ continue
123
+ seen.add(word)
124
+ out.append(word)
125
+ if len(out) >= limit:
126
+ break
127
+ return out
128
+
129
+
130
+ def dimension_of(topic):
131
+ """value_order dimension for a topic, or None (longest keyword wins)."""
132
+ key = str(topic or "").lower()
133
+ if not key:
134
+ return None
135
+ best = None
136
+ best_len = 0
137
+ for dim, keywords in TOPIC_DIMENSIONS:
138
+ for word in keywords:
139
+ if word in key and len(word) > best_len:
140
+ best, best_len = dim, len(word)
141
+ return best
142
+
143
+
144
+ def accumulate(counts=None, entries=None):
145
+ """Add one count per entry per topic; returns a new dict (never mutates)."""
146
+ table = {}
147
+ if isinstance(counts, dict):
148
+ for key, value in counts.items():
149
+ table[str(key)] = int(_num(value, 0))
150
+ for entry in entries or []:
151
+ text = entry.get("text") if isinstance(entry, dict) else str(entry or "")
152
+ if not text:
153
+ continue
154
+ for topic in set(topics(text)):
155
+ table[topic] = table.get(topic, 0) + 1
156
+ return table
157
+
158
+
159
+ def count_topics(entries):
160
+ """Cumulative topic counts for a whole memory list."""
161
+ return accumulate({}, entries)
162
+
163
+
164
+ def decay_counts(counts=None, days=0.0, half_life_days=60.0):
165
+ """Halve the counts once per half-life; keeps a fading theme fading."""
166
+ days = max(0.0, _num(days, 0.0))
167
+ half_life = max(1e-6, _num(half_life_days, 60.0))
168
+ factor = 0.5 ** (days / half_life)
169
+ table = {}
170
+ if isinstance(counts, dict):
171
+ for key, value in counts.items():
172
+ table[str(key)] = round(max(0.0, _num(value, 0.0)) * factor, 4)
173
+ return table
174
+
175
+
176
+ def step(weights=None, counts=None, last_counts=None, days=1.0,
177
+ enabled=True, threshold=TOPIC_THRESHOLD, per_repeat=PER_REPEAT,
178
+ daily_cap=DAILY_CAP, decay_per_day=DECAY_PER_DAY):
179
+ """One drift step. Call once per day with the counts seen so far.
180
+
181
+ weights the current value table (normalized copy is returned)
182
+ counts cumulative topic occurrences in memory
183
+ last_counts the counts watermark from the previous step
184
+ days elapsed days since the previous step (decay only)
185
+ """
186
+ if _vo is None:
187
+ return {"enabled": bool(enabled), "weights": dict(weights or {}),
188
+ "drifted": {}, "decayed": [], "active": {}, "counts": {},
189
+ "reason": "value_order_unavailable"}
190
+ base = _vo.normalize(weights)
191
+ result = {"enabled": bool(enabled), "weights": base, "drifted": {},
192
+ "decayed": [], "active": {}, "counts": {}, "reason": "ok"}
193
+ if not enabled:
194
+ result["reason"] = "disabled"
195
+ return result
196
+
197
+ counts = counts if isinstance(counts, dict) else {}
198
+ last = last_counts if isinstance(last_counts, dict) else {}
199
+ days = max(0.0, _num(days, 1.0))
200
+ threshold = max(1, int(_num(threshold, TOPIC_THRESHOLD)))
201
+ per_repeat = _clamp(per_repeat, 0.0, DAILY_CAP)
202
+ cap = _clamp(daily_cap, 0.0, 0.05)
203
+ decay = _clamp(decay_per_day, 0.0, DAILY_CAP)
204
+ dimensions = _dimensions()
205
+ defaults = _defaults()
206
+
207
+ fresh = {}
208
+ active = {}
209
+ for topic, total in counts.items():
210
+ total = _num(total, 0.0)
211
+ if total <= threshold:
212
+ continue
213
+ active[str(topic)] = total
214
+ new = max(0.0, total - _num(last.get(topic), 0.0))
215
+ if new:
216
+ fresh[str(topic)] = new
217
+ result["active"] = active
218
+
219
+ by_dim = {}
220
+ for topic, new in fresh.items():
221
+ dim = dimension_of(topic)
222
+ if dim is None:
223
+ continue
224
+ by_dim[dim] = by_dim.get(dim, 0.0) + per_repeat * new
225
+ active_dims = {dimension_of(topic) for topic in active}
226
+ for dim, raw in by_dim.items():
227
+ delta = min(cap, raw)
228
+ if delta <= 0:
229
+ continue
230
+ before = base[dim]
231
+ base[dim] = round(_clamp(before + delta, _vo.WEIGHT_FLOOR,
232
+ _vo.WEIGHT_CEIL), 4)
233
+ moved = round(base[dim] - before, 4)
234
+ if moved:
235
+ result["drifted"][dim] = moved
236
+
237
+ if decay > 0 and days > 0:
238
+ relax = min(cap * days, decay * days)
239
+ for dim in dimensions:
240
+ if dim in active_dims or dim in result["drifted"]:
241
+ continue
242
+ before = base[dim]
243
+ default = _num(defaults.get(dim), before)
244
+ if abs(before - default) <= 1e-9:
245
+ continue
246
+ toward = 1.0 if default > before else -1.0
247
+ base[dim] = round(_clamp(before + toward * min(relax, abs(default - before)),
248
+ _vo.WEIGHT_FLOOR, _vo.WEIGHT_CEIL), 4)
249
+ if abs(base[dim] - before) > 1e-9:
250
+ result["decayed"].append(dim)
251
+
252
+ result["weights"] = base
253
+ result["counts"] = {str(key): _num(value, 0.0)
254
+ for key, value in counts.items()}
255
+ return result
@@ -0,0 +1 @@
1
+ # -*- coding: utf-8 -*-
@@ -0,0 +1,63 @@
1
+ import assert from 'node:assert'
2
+ import { CORE_MODES, coreModeButtons, describeBridgeError, coreModeConfirmText, coreModeSuccessText, coreBridgeLabel, createCoreModeFlow } from '../core_mode_ui.mjs'
3
+ assert.deepEqual(CORE_MODES, ['off', 'shadow', 'primary'])
4
+ assert.deepEqual(coreModeButtons('off'), { shadow: { disabled: false }, primary: { disabled: false }, rollback: { disabled: true } })
5
+ assert.deepEqual(coreModeButtons('shadow'), { shadow: { disabled: true }, primary: { disabled: false }, rollback: { disabled: false } })
6
+ assert.deepEqual(coreModeButtons('primary'), { shadow: { disabled: false }, primary: { disabled: true }, rollback: { disabled: false } })
7
+ assert.equal(coreModeButtons('bogus').rollback.disabled, false) // unknown mode -> all switches enabled
8
+ assert.equal(coreModeButtons(undefined).shadow.disabled, false)
9
+ assert.equal(coreModeButtons('primary').rollback.disabled, false)
10
+ assert.equal(describeBridgeError({ status: 403, message: 'forbidden' }), 'HTTP 403:forbidden')
11
+ assert.equal(describeBridgeError({ statusCode: 400, code: 'invalid_params', message: 'invalid mode' }), 'HTTP 400 invalid_params:invalid mode')
12
+ assert.equal(describeBridgeError(new Error('boom')), 'boom')
13
+ assert.equal(describeBridgeError({ payload: { code: 'integrity_refused', message: 'refused' } }), 'integrity_refused:refused')
14
+ assert.equal(coreModeConfirmText('shadow'), '确认切到影子模式?')
15
+ assert.equal(coreModeConfirmText('off'), '确认回退到旧版引擎?')
16
+ assert.ok(coreModeConfirmText('primary').includes('核心为主'))
17
+ assert.equal(coreModeSuccessText('shadow'), '已切到影子模式')
18
+ assert.equal(coreModeSuccessText('off'), '已回退旧版引擎')
19
+ assert.equal(coreModeSuccessText('primary', true), '已切到核心模式(重启后生效)')
20
+ assert.equal(coreBridgeLabel(true), '桥接:已连接')
21
+ assert.equal(coreBridgeLabel(false), '桥接:未连接(静态预览)')
22
+ // DOM stub: inline confirm flow (no window.confirm anywhere)
23
+ function makeView({ bridge = true, post = async () => ({ ok: true, mode: 'shadow' }) } = {}) {
24
+ const state = { confirm: null, toast: null, error: null, posts: 0 }
25
+ return {
26
+ state,
27
+ setConfirm(value) { state.confirm = value },
28
+ setToast(value) { state.toast = value },
29
+ setError(value) { state.error = value },
30
+ bridgeAvailable() { return bridge },
31
+ buttons() { return { shadow: { disabled: false }, primary: { disabled: false }, off: { disabled: true } } },
32
+ post(mode) { state.posts += 1; return post(mode) }
33
+ }
34
+ }
35
+ const view = makeView()
36
+ const flow = createCoreModeFlow({ post: (mode) => view.post(mode), view })
37
+ const clicked = flow.click('shadow')
38
+ assert.equal(clicked.showConfirm, true)
39
+ assert.equal(view.state.confirm, '确认切到影子模式?')
40
+ assert.equal(flow.click('off').disabled, true) // off button disabled -> no confirm
41
+ flow.cancel()
42
+ assert.equal(view.state.confirm, null)
43
+ assert.equal(flow.calls, 0)
44
+ flow.click('shadow')
45
+ const okRun = await flow.confirm()
46
+ assert.equal(okRun.called, true)
47
+ assert.equal(flow.calls, 1)
48
+ assert.equal(view.state.posts, 1)
49
+ assert.equal(view.state.toast, '已切到影子模式')
50
+ const errView = makeView({ post: async () => { const e = new Error('forbidden'); e.status = 403; throw e } })
51
+ const errFlow = createCoreModeFlow({ post: (mode) => errView.post(mode), view: errView })
52
+ errFlow.click('primary')
53
+ await errFlow.confirm()
54
+ assert.ok(errView.state.toast.includes('HTTP 403'), errView.state.toast)
55
+ assert.ok(errView.state.error.includes('403'))
56
+ const offView = makeView({ bridge: false })
57
+ const offFlow = createCoreModeFlow({ post: (mode) => offView.post(mode), view: offView })
58
+ offFlow.click('shadow')
59
+ const offRun = await offFlow.confirm()
60
+ assert.equal(offRun.called, false)
61
+ assert.ok(offView.state.toast.includes('未连接'), offView.state.toast)
62
+ assert.equal(offView.state.posts, 0)
63
+ console.log('CORE MODE UI OK (23 checks)')