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,125 @@
1
+ /** task-222: adapter side of autonomous actions -- execute + record only.
2
+ *
3
+ * The core (biocore_runtime.autonomy) decides WHAT and WHEN; this module just
4
+ * performs the returned action and appends the result to <dataDir>/actions.jsonl.
5
+ * No limit, drive or quiet-hour logic lives here.
6
+ */
7
+ import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
8
+ import { dirname, join } from 'node:path'
9
+
10
+ export const ACTIONS_FILE = 'actions.jsonl'
11
+ export const ACTIONS_KEEP = 500
12
+
13
+ export function actionsPath(dataDir) {
14
+ return join(String(dataDir || '').trim(), ACTIONS_FILE)
15
+ }
16
+
17
+ function appendAction(dataDir, row) {
18
+ const path = actionsPath(dataDir)
19
+ try {
20
+ mkdirSync(dirname(path), { recursive: true })
21
+ appendFileSync(path, JSON.stringify(row) + '\n', 'utf8')
22
+ const lines = readFileSync(path, 'utf8').split('\n').filter(Boolean)
23
+ if (lines.length > ACTIONS_KEEP) {
24
+ writeFileSync(path, lines.slice(-ACTIONS_KEEP).join('\n') + '\n', 'utf8')
25
+ }
26
+ return true
27
+ } catch {
28
+ return false
29
+ }
30
+ }
31
+
32
+ async function deliverSend(ctx, action, log) {
33
+ try {
34
+ const controller = ctx && ctx.sessionController
35
+ if (!controller || typeof controller.resolveAgent !== 'function') {
36
+ return { delivered: false, reason: 'no_session_controller' }
37
+ }
38
+ const resolved = await controller.resolveAgent(String(action.session_id || ''))
39
+ const agent = resolved && resolved.agent
40
+ if (!agent || typeof agent.followup !== 'function') {
41
+ return { delivered: false, reason: 'no_agent' }
42
+ }
43
+ agent.followup({
44
+ id: 'bio-auto-' + Date.now(),
45
+ role: 'user',
46
+ content: [{ type: 'text', text: String(action.text || '') }],
47
+ source: { kind: 'biocore-autonomy' },
48
+ })
49
+ try { await ctx.sessions.flush(agent.session) } catch { /* best effort */ }
50
+ return { delivered: true }
51
+ } catch (error) {
52
+ try { log?.warn?.('[biocore] autonomy send failed: ' + String(error)) } catch { /* ignore */ }
53
+ return { delivered: false, reason: 'send_failed' }
54
+ }
55
+ }
56
+
57
+ /** Execute one core action; returns {ok, kind, result}. Never throws. */
58
+ export async function executeAction(ctx, action, options) {
59
+ const opts = options || {}
60
+ const memory = typeof opts.memoryTool === 'function' ? opts.memoryTool : null
61
+ const log = opts.log
62
+ const kind = String((action && action.kind) || '')
63
+ const sessionId = String((action && action.session_id) || 'autonomy')
64
+ try {
65
+ if (kind === 'send') {
66
+ const result = await deliverSend(ctx, action, log)
67
+ return { ok: result.delivered === true, kind, result }
68
+ }
69
+ if (kind === 'write' && memory) {
70
+ const result = await memory(opts.store, sessionId, 'biocore_remember',
71
+ { text: String(action.text || '') }, opts.rerankConfig)
72
+ return { ok: Boolean(result && result.ok), kind, result: result || {} }
73
+ }
74
+ if (kind === 'retrieve' && memory) {
75
+ const result = await memory(opts.store, sessionId, 'biocore_recall',
76
+ { query: String(action.query || '最近'), limit: 3 }, opts.rerankConfig)
77
+ const count = result && Array.isArray(result.results) ? result.results.length : 0
78
+ return { ok: Boolean(result && result.ok), kind,
79
+ result: { count } }
80
+ }
81
+ return { ok: false, kind, result: { reason: 'unsupported_action' } }
82
+ } catch (error) {
83
+ return { ok: false, kind, result: { reason: 'execution_failed',
84
+ detail: String(error).slice(0, 120) } }
85
+ }
86
+ }
87
+
88
+ /** One tick: ask the core, execute its single action, record the outcome. */
89
+ export async function runAutonomyTick(ctx, options) {
90
+ const opts = options || {}
91
+ const runAction = typeof opts.runAction === 'function' ? opts.runAction : null
92
+ if (!runAction) return { ok: false, reason: 'no_core' }
93
+ let decision = null
94
+ try {
95
+ decision = await runAction('autonomy_tick', {
96
+ store: opts.store,
97
+ data_dir: opts.dataDir,
98
+ session_id: opts.sessionId || 'autonomy',
99
+ last_human_ts: opts.lastHumanTs || 0,
100
+ })
101
+ } catch { decision = null }
102
+ if (!decision || decision.ok !== true) {
103
+ appendAction(opts.dataDir, { ts: Date.now(), kind: 'none',
104
+ reason: 'core_unavailable', result: { ok: false } })
105
+ return { ok: false, reason: 'core_unavailable' }
106
+ }
107
+ if (!decision.action) {
108
+ const reason = String(decision.reason || 'idle')
109
+ if (reason.indexOf('limit') >= 0 || reason.indexOf('decayed') >= 0
110
+ || reason === 'disabled') {
111
+ appendAction(opts.dataDir, { ts: Date.now(), kind: 'none',
112
+ reason, result: { ok: true, stopped: true } })
113
+ }
114
+ return { ok: true, action: null, reason }
115
+ }
116
+ const executed = await executeAction(ctx, decision.action, opts)
117
+ appendAction(opts.dataDir, {
118
+ ts: Date.now(),
119
+ kind: String(decision.action.kind || ''),
120
+ args: decision.action,
121
+ reason: String(decision.reason || ''),
122
+ result: executed.result,
123
+ })
124
+ return { ok: true, action: decision.action, executed }
125
+ }
@@ -0,0 +1,48 @@
1
+ /** Display-only reader for <dataDir>/cache_stats.json.
2
+ *
3
+ * task-223: the adapter no longer computes usage-derived stats; the UI only
4
+ * reads whatever the file already contains. Missing/corrupt -> zeros.
5
+ */
6
+ import { readFileSync } from 'node:fs'
7
+ import { join } from 'node:path'
8
+
9
+ export const CACHE_STATS_FILE = 'cache_stats.json'
10
+ export const CACHE_STATS_DEFAULTS = Object.freeze({
11
+ turns: 0,
12
+ prompt_tokens: 0,
13
+ output_tokens: 0,
14
+ cache_read: 0,
15
+ cache_write: 0,
16
+ hit_rate: 0,
17
+ updated_at: 0,
18
+ })
19
+
20
+ function num(value) {
21
+ const parsed = Number(value)
22
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : 0
23
+ }
24
+
25
+ export function cacheStatsPath(dataDir) {
26
+ return join(String(dataDir || '').trim(), CACHE_STATS_FILE)
27
+ }
28
+
29
+ /** Read the file as-is (numbers default to 0); never throws. */
30
+ export function readCacheStats(dataDir) {
31
+ try {
32
+ const doc = JSON.parse(readFileSync(cacheStatsPath(dataDir), 'utf8'))
33
+ if (!doc || typeof doc !== 'object' || Array.isArray(doc)) {
34
+ return { ...CACHE_STATS_DEFAULTS }
35
+ }
36
+ return {
37
+ turns: Math.round(num(doc.turns)),
38
+ prompt_tokens: Math.round(num(doc.prompt_tokens)),
39
+ output_tokens: Math.round(num(doc.output_tokens)),
40
+ cache_read: Math.round(num(doc.cache_read)),
41
+ cache_write: Math.round(num(doc.cache_write)),
42
+ hit_rate: num(doc.hit_rate),
43
+ updated_at: Math.round(num(doc.updated_at)),
44
+ }
45
+ } catch {
46
+ return { ...CACHE_STATS_DEFAULTS }
47
+ }
48
+ }
@@ -19,7 +19,6 @@ import { join } from 'node:path'
19
19
  import { readStateFile, sessionStateFile } from './state.js'
20
20
  import { pendingSessionTurn, queueSessionTurn } from './turn-queue.js'
21
21
  import {
22
- anySwitchOn,
23
22
  promptTextFor,
24
23
  rememberPromptText,
25
24
  sessionIdOf,
@@ -35,7 +34,7 @@ import {
35
34
  } from './sanitize-reply.js'
36
35
 
37
36
  export const TURN_CONTEXT_NAME = 'biocore-expression-turn'
38
- export const TURN_CONTEXT_ORDER = -190
37
+ export const TURN_CONTEXT_ORDER = 9500
39
38
  export const CORE_WARN_PREFIX = '__bioCoreWarn'
40
39
  export const CORE_GATE_PREFIX = '__bioGate'
41
40
  export const CORE_TRACE_PREFIX = '__bioTrace'
@@ -107,6 +106,21 @@ export function chooseCleanReply(original, coreCleanText, options) {
107
106
  return String(sanitize(source))
108
107
  }
109
108
 
109
+ /** Delegation depth from the DSH session header; missing -> 0 (main). */
110
+ export function delegationDepthOf(session) {
111
+ const header = session && session.header && typeof session.header === 'object'
112
+ ? session.header : {}
113
+ const fromHeader = Number(header.delegationDepth)
114
+ if (Number.isFinite(fromHeader)) return Math.max(0, Math.trunc(fromHeader))
115
+ const direct = Number(session && session.delegationDepth)
116
+ if (Number.isFinite(direct)) return Math.max(0, Math.trunc(direct))
117
+ return 0
118
+ }
119
+
120
+ export function isSubagentSession(session) {
121
+ return delegationDepthOf(session) >= 1
122
+ }
123
+
110
124
  export function personaApplied(dataDir) {
111
125
  try {
112
126
  const status = JSON.parse(readFileSync(join(String(dataDir || '').trim(), 'persona_preset.json'), 'utf8'))
@@ -186,8 +200,8 @@ export function makeCoreSessionListener(options) {
186
200
  if (type !== 'user/message' && type !== 'assistant/message') return
187
201
  const sessionId = session && session.id != null ? stringId(session.id) : ''
188
202
  if (!sessionId) return
203
+ const subagent = isSubagentSession(session)
189
204
  const switches = getSwitches()
190
- if (!anySwitchOn(switches)) return
191
205
  if (type === 'user/message') {
192
206
  const data = event.data || {}
193
207
  const source = data.source || {}
@@ -207,9 +221,15 @@ export function makeCoreSessionListener(options) {
207
221
  core = { ok: false, reason: 'core_exception' }
208
222
  }
209
223
  if (core && core.ok === true && typeof core.prompt_block === 'string') {
210
- rememberPromptText(sessionId, 'turn', core.prompt_block)
224
+ // Subagent sessions must not receive persona/state/thinking rules:
225
+ // keep turn_start for state capture, but blank the -190 prompt.
226
+ rememberPromptText(sessionId, 'turn', subagent ? '' : core.prompt_block)
211
227
  return core
212
228
  }
229
+ if (subagent) {
230
+ rememberPromptText(sessionId, 'turn', '')
231
+ return null
232
+ }
213
233
  warnOnce((core && (core.reason || core.detail)) || 'turn_start_failed')
214
234
  try {
215
235
  const stateFile = sessionStateFile(stateDir, sessionId)
@@ -293,7 +313,6 @@ export function registerCoreTurn(ctx, options) {
293
313
  order: TURN_CONTEXT_ORDER,
294
314
  text: (context) => {
295
315
  try {
296
- if (!anySwitchOn(getSwitches())) return ''
297
316
  const sessionId = sessionIdOf(context)
298
317
  return sessionId ? sanitizeTemplateVars(promptTextFor(sessionId, 'turn')) : ''
299
318
  } catch { return '' }
@@ -310,7 +329,6 @@ export function registerCoreTurn(ctx, options) {
310
329
  let result = await next()
311
330
  if (!result || typeof result !== 'object') result = assembly
312
331
  try {
313
- if (!anySwitchOn(getSwitches())) return result
314
332
  const sessionId = sessionIdOf(context)
315
333
  if (!sessionId) return result
316
334
  // turn_start was queued by session/event; wait for it, then patch the
@@ -32,15 +32,6 @@ export function sessionIdOf(context) {
32
32
  } catch { return '' }
33
33
  }
34
34
 
35
- /** True when any of the four flags.json switches is on. */
36
- export function anySwitchOn(switches) {
37
- try {
38
- const value = switches && typeof switches === 'object' ? switches : {}
39
- return value.safety_enabled === true || value.boundary_enabled === true
40
- || value.crisis_safety_enabled === true || value.proactive_send === true
41
- } catch { return false }
42
- }
43
-
44
35
  /** Cache one python-produced prompt text ('' clears it). */
45
36
  export function rememberPromptText(sessionId, phase, text) {
46
37
  try {
@@ -98,7 +89,6 @@ export function registerExpressionInjections(ctx, options) {
98
89
  order,
99
90
  text: (context) => {
100
91
  try {
101
- if (!anySwitchOn(getSwitches())) return ''
102
92
  const sessionId = sessionIdOf(context)
103
93
  if (!sessionId) return ''
104
94
  return String(getText(phase, sessionId) || '')
@@ -9,7 +9,6 @@ import { readStateFile, sessionStateFile, projectState } from './state.js'
9
9
  import {
10
10
  BOOT_CONTEXT_NAME,
11
11
  TURN_CONTEXT_NAME,
12
- anySwitchOn,
13
12
  promptTextFor,
14
13
  rememberPromptText,
15
14
  clearPromptText,
@@ -39,10 +38,6 @@ export async function refreshPromptCache(options) {
39
38
  const callInject = typeof opts.callInject === 'function' ? opts.callInject : null
40
39
  if (!sessionId || !callInject) return { refreshed: false, reason: 'unavailable' }
41
40
  const switches = getSwitches()
42
- if (!anySwitchOn(switches)) {
43
- clearPromptText(sessionId)
44
- return { refreshed: false, reason: 'switches_off' }
45
- }
46
41
  const userText = lastUserFor(sessionId)
47
42
  const state = statePayloadFor(dataDir, sessionId)
48
43
  const phases = []
@@ -106,10 +101,6 @@ export function registerContextRefresh(ctx, options) {
106
101
  const switches = getSwitches()
107
102
  const sessionId = sessionIdOf(context)
108
103
  if (!sessionId) return result
109
- if (!anySwitchOn(switches)) {
110
- clearPromptText(sessionId)
111
- return result
112
- }
113
104
  // One turn may still be writing the state file; wait for it so python
114
105
  // sees the state the user message just produced.
115
106
  await pendingSessionTurn(sessionId)
@@ -57,7 +57,8 @@ export function registerMemoryCapture(ctx, getConfig, storeDir, options = {}) {
57
57
  min_chars: config.memoryMinChars,
58
58
  })
59
59
  if (result && result.ok === true && result.captured === true && injectionState) {
60
- await refreshInjection(storeDir, sessionId, config, injectionState)
60
+ const rerank = typeof options.getRerankConfig === 'function' ? options.getRerankConfig() : null
61
+ await refreshInjection(storeDir, sessionId, config, injectionState, rerank)
61
62
  }
62
63
  }).catch(() => { /* capture never disturbs a turn */ })
63
64
  } catch { /* contained */ }
@@ -3,11 +3,16 @@
3
3
  * DSH asks for prompt context synchronously, so this module only caches the
4
4
  * text returned by the python bridge per session; formatting and truncation
5
5
  * never happen in Node.
6
+ *
7
+ * task-221: refresh uses the user's own words (mode=query) so retrieval is a
8
+ * query recall, not "newest N"; python falls back to the recent ordering when
9
+ * the engine/embedding/rerank chain is unavailable.
6
10
  */
7
11
  import { memoryInject } from '../py-bridge.js'
8
12
 
9
13
  export const CONTEXT_NAME = 'biocore-memory'
10
- export const CONTEXT_ORDER = -190
14
+ export const CONTEXT_ORDER = 9510
15
+ export const DEFAULT_TOP_K = 5
11
16
 
12
17
  export function createInjectionState() {
13
18
  return new Map()
@@ -29,22 +34,41 @@ export function sessionIdOf(context) {
29
34
  } catch { return '' }
30
35
  }
31
36
 
37
+ function textOfMessage(message) {
38
+ try {
39
+ const blocks = message && Array.isArray(message.content) ? message.content : []
40
+ const parts = []
41
+ for (const block of blocks) {
42
+ if (block && block.type === 'text' && typeof block.text === 'string') parts.push(block.text)
43
+ }
44
+ return parts.join('\n').replace(/\s+/g, ' ').trim().slice(0, 2000)
45
+ } catch { return '' }
46
+ }
47
+
48
+ function topKOf(config) {
49
+ const raw = Number(config && config.injectTopK)
50
+ if (!Number.isFinite(raw)) return DEFAULT_TOP_K
51
+ return Math.max(1, Math.min(10, Math.round(raw)))
52
+ }
53
+
32
54
  /** Reload one session summary from the python pipeline and cache it. */
33
- export async function refreshInjection(storeDir, sessionId, config, state) {
55
+ export async function refreshInjection(storeDir, sessionId, config, state, rerankConfig,
56
+ query) {
34
57
  if (!state || typeof state.set !== 'function') return ''
35
58
  if (config && config.memoryEnabled === false) {
36
59
  state.set(String(sessionId), '')
37
60
  return ''
38
61
  }
39
62
  const result = await memoryInject(storeDir, sessionId,
40
- config && config.injectMaxChars, 5)
63
+ config && config.injectMaxChars, topKOf(config), rerankConfig, query)
41
64
  const text = result && result.ok === true ? String(result.text || '') : ''
42
65
  state.set(String(sessionId), text)
43
66
  return text
44
67
  }
45
68
 
46
- /** Register the per-session cached text as a dynamic system prompt context. */
47
- export function registerMemoryInjection(ctx, getConfig, storeDir, state) {
69
+ /** Register the per-session cached text + the user-message refresh listener. */
70
+ export function registerMemoryInjection(ctx, getConfig, storeDir, state, options) {
71
+ const opts = options || {}
48
72
  if (!ctx || typeof ctx.inject !== 'function') return null
49
73
  const cfg = () => {
50
74
  try { return (typeof getConfig === 'function' ? getConfig() : getConfig) || {} } catch { return {} }
@@ -70,5 +94,26 @@ export function registerMemoryInjection(ctx, getConfig, storeDir, state) {
70
94
  } catch { /* prompt context rejected */ }
71
95
  })
72
96
  } catch { /* cordis inject unavailable */ }
97
+ try {
98
+ if (typeof ctx.on === 'function') {
99
+ const dispose = ctx.on('session/event', (session, event) => {
100
+ try {
101
+ if (!event || event.type !== 'user/message') return
102
+ const data = event.data || {}
103
+ const source = data.source || {}
104
+ if (source.kind !== 'user') return
105
+ const sessionId = String((session && session.id) || '')
106
+ const text = textOfMessage(data)
107
+ const config = cfg()
108
+ if (!sessionId || !text || config.memoryEnabled === false) return
109
+ const rerank = typeof opts.getRerankConfig === 'function'
110
+ ? opts.getRerankConfig() : null
111
+ void refreshInjection(storeDir, sessionId, config, state, rerank,
112
+ text, 'query')
113
+ } catch { /* a refresh never disturbs a turn */ }
114
+ })
115
+ if (typeof dispose === 'function') disposers.push(dispose)
116
+ }
117
+ } catch { /* session/event unavailable */ }
73
118
  return () => { for (const dispose of disposers) { try { dispose() } catch { /* ignore */ } } }
74
119
  }
@@ -25,13 +25,14 @@ export function sessionIdFromExec(exec) {
25
25
  } catch { return 'global' }
26
26
  }
27
27
 
28
- export function buildMemoryToolDefinitions(storeDir, getConfig) {
28
+ export function buildMemoryToolDefinitions(storeDir, getConfig, getRerankConfig) {
29
29
  const cfg = () => {
30
30
  try { return (typeof getConfig === 'function' ? getConfig() : getConfig) || {} } catch { return {} }
31
31
  }
32
32
  const run = async (name, args, exec) => {
33
33
  if (cfg().memoryEnabled === false) return { ok: false, reason: 'memory_disabled' }
34
- const response = await memoryTool(storeDir, sessionIdFromExec(exec), name, args || {})
34
+ const rerank = typeof getRerankConfig === 'function' ? getRerankConfig() : null
35
+ const response = await memoryTool(storeDir, sessionIdFromExec(exec), name, args || {}, rerank)
35
36
  const flat = response && response.result && typeof response.result === 'object'
36
37
  ? response.result : {}
37
38
  if (response && response.ok === true) return { ok: true, ...flat }
@@ -83,9 +84,11 @@ export function buildMemoryToolDefinitions(storeDir, getConfig) {
83
84
  ]
84
85
  }
85
86
 
86
- export function registerMemoryTools(ctx, getConfig, storeDir) {
87
+ export function registerMemoryTools(ctx, getConfig, storeDir, options) {
87
88
  if (!ctx || typeof ctx.inject !== 'function') return null
88
- const definitions = buildMemoryToolDefinitions(storeDir, getConfig)
89
+ const opts = options || {}
90
+ const getRerankConfig = typeof opts.getRerankConfig === 'function' ? opts.getRerankConfig : null
91
+ const definitions = buildMemoryToolDefinitions(storeDir, getConfig, getRerankConfig)
89
92
  const disposers = []
90
93
  const registered = []
91
94
  try {
@@ -0,0 +1,46 @@
1
+ /** config.json -> Python env / rerank payload (read-only, fail-silent). */
2
+ import { readFileSync } from 'node:fs'
3
+ import { join } from 'node:path'
4
+
5
+ function readConfig(dataDir) {
6
+ try {
7
+ const doc = JSON.parse(readFileSync(join(String(dataDir || '').trim(), 'config.json'), 'utf8'))
8
+ return doc && typeof doc === 'object' && !Array.isArray(doc) ? doc : {}
9
+ } catch { return {} }
10
+ }
11
+
12
+ const MODES = new Set(['auto', 'remote', 'local', 'off'])
13
+
14
+ /** Env for embedding; local_path is intentionally not exported. */
15
+ export function embeddingEnv(dataDir) {
16
+ const doc = readConfig(dataDir)
17
+ const embedding = doc.embedding && typeof doc.embedding === 'object' ? doc.embedding : {}
18
+ const mode = String(embedding.mode || 'auto').trim()
19
+ const env = {
20
+ BIOCORE_EMBEDDING_MODE: MODES.has(mode) ? mode : 'auto',
21
+ BIOCORE_EMBEDDING_BASE_URL: String(embedding.base_url || '').trim(),
22
+ BIOCORE_EMBEDDING_MODEL: String(embedding.model || '').trim(),
23
+ BIOCORE_EMBEDDING_KEY: typeof embedding.api_key === 'string' ? embedding.api_key : '',
24
+ // Cross-process disk cache so spawn-per-call still hits (task-221).
25
+ BIOCORE_EMBEDDING_CACHE_FILE: dataDir ? join(String(dataDir).trim(), 'cache') : '',
26
+ // Counters survive the spawn-per-call bridge; /stats reads this file.
27
+ BIOCORE_EMBED_STATS_FILE: dataDir ? join(String(dataDir).trim(), 'embed_stats.json') : '',
28
+ }
29
+ for (const key of Object.keys(env)) if (!env[key]) delete env[key]
30
+ return env
31
+ }
32
+
33
+ /** rerank_config for memory_inject / recall payloads. */
34
+ export function rerankConfig(dataDir) {
35
+ const doc = readConfig(dataDir)
36
+ const rerank = doc.rerank && typeof doc.rerank === 'object' ? doc.rerank : {}
37
+ const top = Number(rerank.top_n)
38
+ return {
39
+ rerank_enabled: rerank.enabled === true,
40
+ rerank_base_url: String(rerank.base_url || '').trim(),
41
+ rerank_model: String(rerank.model || '').trim(),
42
+ rerank_api_key: typeof rerank.api_key === 'string' ? rerank.api_key : '',
43
+ rerank_top_n: Number.isFinite(top) ? Math.max(1, Math.min(100, Math.round(top))) : 20,
44
+ rerank_timeout: 8,
45
+ }
46
+ }
@@ -8,8 +8,6 @@
8
8
  *
9
9
  * All four flags off -> no call, no log, no mutation (old behaviour).
10
10
  */
11
- import { anySwitchOn } from './expression-inject.js'
12
-
13
11
  export const GATE_LOG_PREFIX = '__bioGate'
14
12
  export const ASSISTANT_EVENT = 'assistant/message'
15
13
  export const USER_EVENT = 'user/message'
@@ -112,7 +110,6 @@ export function registerSafetyGate(ctx, options) {
112
110
  }
113
111
  if (kind !== ASSISTANT_EVENT) return
114
112
  const switches = getSwitches()
115
- if (!anySwitchOn(switches)) return
116
113
  const reply = assistantTextOf(event)
117
114
  if (!reply) return
118
115
  const decision = await runGate({
@@ -0,0 +1,54 @@
1
+ /** task-223: GET /api/biocore/series -- pure forward to the core.
2
+ *
3
+ * The core assembles the points (global history + growth history). This file
4
+ * only forwards the request and returns the reply unchanged; when the core is
5
+ * unreachable it answers an empty series so the panel never sees a 404.
6
+ */
7
+ import { runtimeAction } from '../py-bridge.js'
8
+
9
+ export const SERIES_ROUTE_PATH = '/api/biocore/series'
10
+ export const SERIES_DEFAULT_LIMIT = 24
11
+ export const SERIES_MAX_LIMIT = 288
12
+
13
+ function sendJson(res, code, payload) {
14
+ try {
15
+ res.writeHead(code, {
16
+ 'content-type': 'application/json; charset=utf-8',
17
+ 'cache-control': 'no-store',
18
+ })
19
+ res.end(JSON.stringify(payload))
20
+ } catch { /* client gone */ }
21
+ return payload
22
+ }
23
+
24
+ export async function handleSeriesRoute(req, res, options) {
25
+ const opts = options || {}
26
+ const method = String((req && req.method) || 'GET').toUpperCase()
27
+ if (method !== 'GET' && method !== 'HEAD') {
28
+ return sendJson(res, 405, { ok: false, reason: 'method_not_allowed' })
29
+ }
30
+ let limit = SERIES_DEFAULT_LIMIT
31
+ try {
32
+ const url = new URL(String(req.url || ''), 'http://local')
33
+ const raw = Number(url.searchParams.get('limit'))
34
+ if (Number.isFinite(raw)) {
35
+ limit = Math.max(1, Math.min(SERIES_MAX_LIMIT, Math.round(raw)))
36
+ }
37
+ } catch { /* default limit */ }
38
+ let reply = null
39
+ try {
40
+ const runAction = typeof opts.runAction === 'function' ? opts.runAction : runtimeAction
41
+ reply = await runAction('series', {
42
+ store: opts.store,
43
+ data_dir: opts.dataDir,
44
+ limit,
45
+ })
46
+ } catch { reply = null }
47
+ if (reply && typeof reply === 'object') return sendJson(res, 200, reply)
48
+ return sendJson(res, 200, {
49
+ ok: true,
50
+ series: { ts: [], v: [], tension: [], warmth: [], heaviness: [], clarity: [] },
51
+ points: [],
52
+ source: 'unavailable',
53
+ })
54
+ }
@@ -27,24 +27,40 @@ export const FLAGS_FILE_NAME = 'flags.json'
27
27
  export const BODY_LIMIT = 4096
28
28
 
29
29
  export const SWITCH_KEYS = Object.freeze([
30
+ 'core_enabled',
30
31
  'safety_enabled',
31
32
  'boundary_enabled',
32
33
  'crisis_safety_enabled',
33
34
  'proactive_send',
35
+ // learning switches (task-221): flags only, never safety semantics
36
+ 'mood_recall',
37
+ 'affect_encoding',
38
+ 'memory_feedback',
39
+ 'preference_drift',
34
40
  ])
35
41
 
36
42
  export const SWITCH_LABELS = Object.freeze({
43
+ core_enabled: '核心',
37
44
  safety_enabled: '安全闸',
38
45
  boundary_enabled: '底线',
39
46
  crisis_safety_enabled: '危机兜底',
40
47
  proactive_send: '主动回复',
48
+ mood_recall: '情绪匹配召回',
49
+ affect_encoding: '编码带情绪',
50
+ memory_feedback: '回忆回灌情绪',
51
+ preference_drift: '主题偏好漂移',
41
52
  })
42
53
 
43
54
  export const DEFAULT_SWITCHES = Object.freeze({
55
+ core_enabled: true,
44
56
  safety_enabled: false,
45
57
  boundary_enabled: false,
46
58
  crisis_safety_enabled: false,
47
59
  proactive_send: false,
60
+ mood_recall: true,
61
+ affect_encoding: true,
62
+ memory_feedback: true,
63
+ preference_drift: true,
48
64
  })
49
65
 
50
66
  /** Read-side aliases for renamed switches (writes always use the canonical key). */