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.
- package/README.md +11 -0
- package/lib/core-worker.js +268 -0
- package/lib/index.js +160 -70
- package/lib/modules/autonomy.js +125 -0
- package/lib/modules/cache-stats.js +48 -0
- package/lib/modules/core-turn.js +24 -6
- package/lib/modules/expression-inject.js +0 -10
- package/lib/modules/expression-refresh.js +0 -9
- package/lib/modules/memory-capture.js +2 -1
- package/lib/modules/memory-inject.js +50 -5
- package/lib/modules/memory-tools.js +7 -4
- package/lib/modules/python-env.js +46 -0
- package/lib/modules/safety-gate.js +0 -3
- package/lib/modules/series.js +54 -0
- package/lib/modules/switch.js +16 -0
- package/lib/py-bridge.js +81 -15
- package/package.json +1 -1
- package/python/biocore_memory/core/config.py +138 -127
- package/python/biocore_memory/engine.py +2653 -2476
- package/python/biocore_memory/recall/embedding.py +129 -0
- package/python/biocore_memory/recall/feedback.py +202 -0
- package/python/biocore_model/preference_drift.py +255 -0
- package/python/biocore_model/tests/__init__.py +1 -0
- package/python/biocore_model/tests/core_mode_ui_check.mjs +63 -0
- package/python/biocore_model/tests/core_status_check.mjs +29 -0
- package/python/biocore_model/tests/dom_safe_check.mjs +22 -0
- package/python/biocore_model/tests/leak_samples_run.py +18 -0
- package/python/biocore_model/tests/orb_ring_check.mjs +20 -0
- package/python/biocore_model/tests/panel_html_check.mjs +63 -0
- package/python/biocore_model/tests/persona_flow_check.mjs +17 -0
- package/python/biocore_model/tests/render_guard_check.mjs +19 -0
- package/python/biocore_model/tests/switch_groups_check.mjs +94 -0
- package/python/biocore_model/tests/tabs_state_check.mjs +24 -0
- package/python/biocore_model/tests/test_affective_labor.py +102 -0
- package/python/biocore_model/tests/test_appraisal.py +137 -0
- package/python/biocore_model/tests/test_appraisal_action.py +55 -0
- package/python/biocore_model/tests/test_arch_convergence.py +35 -0
- package/python/biocore_model/tests/test_attachment_behavior.py +95 -0
- package/python/biocore_model/tests/test_attachment_regulation.py +63 -0
- package/python/biocore_model/tests/test_attachment_state.py +29 -0
- package/python/biocore_model/tests/test_attribution.py +56 -0
- package/python/biocore_model/tests/test_attributor.py +108 -0
- package/python/biocore_model/tests/test_autonomic.py +69 -0
- package/python/biocore_model/tests/test_autonomy_accumulator.py +84 -0
- package/python/biocore_model/tests/test_autonomy_affect.py +62 -0
- package/python/biocore_model/tests/test_autonomy_blame.py +44 -0
- package/python/biocore_model/tests/test_autonomy_caps.py +29 -0
- package/python/biocore_model/tests/test_autonomy_check.py +40 -0
- package/python/biocore_model/tests/test_autonomy_clingy.py +41 -0
- package/python/biocore_model/tests/test_autonomy_cooldown_selfcontact.py +138 -0
- package/python/biocore_model/tests/test_autonomy_frustration.py +42 -0
- package/python/biocore_model/tests/test_autonomy_hourly_limit.py +46 -0
- package/python/biocore_model/tests/test_autonomy_loop.py +64 -0
- package/python/biocore_model/tests/test_autonomy_mode.py +37 -0
- package/python/biocore_model/tests/test_autonomy_model.py +60 -0
- package/python/biocore_model/tests/test_autonomy_model_v2.py +68 -0
- package/python/biocore_model/tests/test_autonomy_module_text.py +104 -0
- package/python/biocore_model/tests/test_autonomy_params.py +163 -0
- package/python/biocore_model/tests/test_autonomy_reason.py +34 -0
- package/python/biocore_model/tests/test_autonomy_self.py +153 -0
- package/python/biocore_model/tests/test_autonomy_send_gate.py +38 -0
- package/python/biocore_model/tests/test_autonomy_silence.py +37 -0
- package/python/biocore_model/tests/test_autonomy_status.py +35 -0
- package/python/biocore_model/tests/test_autonomy_test_trigger.py +24 -0
- package/python/biocore_model/tests/test_autonomy_tick.py +148 -0
- package/python/biocore_model/tests/test_autonomy_v4.py +64 -0
- package/python/biocore_model/tests/test_autonomy_v5.py +43 -0
- package/python/biocore_model/tests/test_autonomy_v7.py +48 -0
- package/python/biocore_model/tests/test_autonomy_v_theta.py +85 -0
- package/python/biocore_model/tests/test_batch1_wiring.py +231 -0
- package/python/biocore_model/tests/test_batch2_wiring.py +231 -0
- package/python/biocore_model/tests/test_batch3_wiring.py +223 -0
- package/python/biocore_model/tests/test_bio_tick.py +202 -0
- package/python/biocore_model/tests/test_boundary.py +183 -0
- package/python/biocore_model/tests/test_boundary_gate.py +295 -0
- package/python/biocore_model/tests/test_brain_split.py +99 -0
- package/python/biocore_model/tests/test_co_recall.py +350 -0
- package/python/biocore_model/tests/test_commitment_extractor.py +197 -0
- package/python/biocore_model/tests/test_compassion_fatigue.py +84 -0
- package/python/biocore_model/tests/test_connection_bids.py +69 -0
- package/python/biocore_model/tests/test_core_bridge.py +88 -0
- package/python/biocore_model/tests/test_core_bridge_episode.py +14 -0
- package/python/biocore_model/tests/test_core_bridge_forget.py +17 -0
- package/python/biocore_model/tests/test_core_bridge_identity.py +15 -0
- package/python/biocore_model/tests/test_core_bridge_media.py +17 -0
- package/python/biocore_model/tests/test_core_bridge_relation.py +17 -0
- package/python/biocore_model/tests/test_core_bridge_summary.py +17 -0
- package/python/biocore_model/tests/test_core_client.py +156 -0
- package/python/biocore_model/tests/test_core_commands.py +146 -0
- package/python/biocore_model/tests/test_core_mode.py +161 -0
- package/python/biocore_model/tests/test_core_state.py +240 -0
- package/python/biocore_model/tests/test_criticality.py +61 -0
- package/python/biocore_model/tests/test_cross_session_copy.py +67 -0
- package/python/biocore_model/tests/test_debug_values.py +169 -0
- package/python/biocore_model/tests/test_defense_mechanisms.py +177 -0
- package/python/biocore_model/tests/test_delta_budget.py +157 -0
- package/python/biocore_model/tests/test_diag_trace.py +201 -0
- package/python/biocore_model/tests/test_directives_compat.py +698 -0
- package/python/biocore_model/tests/test_dissonance.py +97 -0
- package/python/biocore_model/tests/test_drift_sign.py +87 -0
- package/python/biocore_model/tests/test_embedding_fallback.py +277 -0
- package/python/biocore_model/tests/test_embedding_local.py +279 -0
- package/python/biocore_model/tests/test_embedding_mode.py +175 -0
- package/python/biocore_model/tests/test_embedding_modes.py +342 -0
- package/python/biocore_model/tests/test_emotion_geometry.py +114 -0
- package/python/biocore_model/tests/test_emotion_probe.py +214 -0
- package/python/biocore_model/tests/test_emotion_probe_concurrency.py +128 -0
- package/python/biocore_model/tests/test_entity_graph.py +198 -0
- package/python/biocore_model/tests/test_entity_resolve.py +446 -0
- package/python/biocore_model/tests/test_envy.py +173 -0
- package/python/biocore_model/tests/test_exit_or_silence.py +209 -0
- package/python/biocore_model/tests/test_expr_guard.py +1 -0
- package/python/biocore_model/tests/test_expr_intent.py +66 -0
- package/python/biocore_model/tests/test_expr_state.py +66 -0
- package/python/biocore_model/tests/test_expr_template.py +71 -0
- package/python/biocore_model/tests/test_expr_tone.py +41 -0
- package/python/biocore_model/tests/test_extreme_scenarios.py +147 -0
- package/python/biocore_model/tests/test_feedback_flow.py +185 -0
- package/python/biocore_model/tests/test_feedback_guard.py +65 -0
- package/python/biocore_model/tests/test_feedback_llm.py +59 -0
- package/python/biocore_model/tests/test_feedback_llm_inferred.py +140 -0
- package/python/biocore_model/tests/test_feedback_memory.py +63 -0
- package/python/biocore_model/tests/test_feedback_user.py +61 -0
- package/python/biocore_model/tests/test_frustration_curve.py +66 -0
- package/python/biocore_model/tests/test_global_bio_state.py +116 -0
- package/python/biocore_model/tests/test_governance.py +181 -0
- package/python/biocore_model/tests/test_granularity.py +133 -0
- package/python/biocore_model/tests/test_graph_extract.py +224 -0
- package/python/biocore_model/tests/test_graph_recall.py +226 -0
- package/python/biocore_model/tests/test_graph_sqlite.py +298 -0
- package/python/biocore_model/tests/test_growth.py +301 -0
- package/python/biocore_model/tests/test_intent_render.py +60 -0
- package/python/biocore_model/tests/test_intero_regulation.py +224 -0
- package/python/biocore_model/tests/test_intero_style.py +146 -0
- package/python/biocore_model/tests/test_item3_numeric.py +105 -0
- package/python/biocore_model/tests/test_item6_numeric.py +116 -0
- package/python/biocore_model/tests/test_layered_memory.py +174 -0
- package/python/biocore_model/tests/test_leak_guard.py +75 -0
- package/python/biocore_model/tests/test_learning_probe.py +93 -0
- package/python/biocore_model/tests/test_life_loop.py +222 -0
- package/python/biocore_model/tests/test_life_loop_10s.py +131 -0
- package/python/biocore_model/tests/test_local_embedding_health.py +258 -0
- package/python/biocore_model/tests/test_memory_affect_encoding.py +79 -0
- package/python/biocore_model/tests/test_memory_backend.py +332 -0
- package/python/biocore_model/tests/test_memory_dedup.py +316 -0
- package/python/biocore_model/tests/test_memory_dual.py +219 -0
- package/python/biocore_model/tests/test_memory_dual_wiring.py +219 -0
- package/python/biocore_model/tests/test_memory_feedback.py +241 -0
- package/python/biocore_model/tests/test_memory_fixes.py +84 -0
- package/python/biocore_model/tests/test_memory_forgetting.py +58 -0
- package/python/biocore_model/tests/test_memory_inject.py +211 -0
- package/python/biocore_model/tests/test_memory_mood_recall.py +118 -0
- package/python/biocore_model/tests/test_memory_store.py +372 -0
- package/python/biocore_model/tests/test_metacog.py +103 -0
- package/python/biocore_model/tests/test_migrate.py +245 -0
- package/python/biocore_model/tests/test_migrate_store.py +194 -0
- package/python/biocore_model/tests/test_mind_wander.py +65 -0
- package/python/biocore_model/tests/test_mood.py +35 -0
- package/python/biocore_model/tests/test_narrative.py +114 -0
- package/python/biocore_model/tests/test_needs_esteem.py +51 -0
- package/python/biocore_model/tests/test_needs_events.py +205 -0
- package/python/biocore_model/tests/test_needs_sleep.py +54 -0
- package/python/biocore_model/tests/test_noise_bank.py +54 -0
- package/python/biocore_model/tests/test_numeric_schema.py +98 -0
- package/python/biocore_model/tests/test_output_protocol.py +65 -0
- package/python/biocore_model/tests/test_page_debug.py +147 -0
- package/python/biocore_model/tests/test_page_status_autonomy.py +215 -0
- package/python/biocore_model/tests/test_pipeline.py +345 -0
- package/python/biocore_model/tests/test_pipeline_api.py +192 -0
- package/python/biocore_model/tests/test_pipeline_mode.py +222 -0
- package/python/biocore_model/tests/test_plugin_config.py +85 -0
- package/python/biocore_model/tests/test_policy_learning.py +116 -0
- package/python/biocore_model/tests/test_possible_selves.py +88 -0
- package/python/biocore_model/tests/test_predictor.py +227 -0
- package/python/biocore_model/tests/test_preference_drift.py +142 -0
- package/python/biocore_model/tests/test_preset.py +142 -0
- package/python/biocore_model/tests/test_proactive_context.py +70 -0
- package/python/biocore_model/tests/test_proactive_history.py +90 -0
- package/python/biocore_model/tests/test_proactive_memory.py +141 -0
- package/python/biocore_model/tests/test_proactive_send_record.py +88 -0
- package/python/biocore_model/tests/test_probe_contract.py +71 -0
- package/python/biocore_model/tests/test_prospective.py +154 -0
- package/python/biocore_model/tests/test_provider_fallback.py +89 -0
- package/python/biocore_model/tests/test_reconstruction.py +111 -0
- package/python/biocore_model/tests/test_regulation_strategies.py +228 -0
- package/python/biocore_model/tests/test_relation_stage.py +114 -0
- package/python/biocore_model/tests/test_relationship_stage.py +59 -0
- package/python/biocore_model/tests/test_release_140.py +55 -0
- package/python/biocore_model/tests/test_repair_after_boundary.py +249 -0
- package/python/biocore_model/tests/test_reply_limit.py +86 -0
- package/python/biocore_model/tests/test_reply_limit_persist.py +48 -0
- package/python/biocore_model/tests/test_reply_split.py +52 -0
- package/python/biocore_model/tests/test_reply_split_astrbot.py +66 -0
- package/python/biocore_model/tests/test_reply_split_inject.py +53 -0
- package/python/biocore_model/tests/test_rerank.py +229 -0
- package/python/biocore_model/tests/test_rollback.py +175 -0
- package/python/biocore_model/tests/test_runtime.py +175 -0
- package/python/biocore_model/tests/test_runtime_active.py +205 -0
- package/python/biocore_model/tests/test_runtime_pipeline.py +1583 -0
- package/python/biocore_model/tests/test_runtime_proactive.py +231 -0
- package/python/biocore_model/tests/test_scheduler.py +79 -0
- package/python/biocore_model/tests/test_secrecy.py +105 -0
- package/python/biocore_model/tests/test_self_esteem.py +45 -0
- package/python/biocore_model/tests/test_self_intent.py +347 -0
- package/python/biocore_model/tests/test_self_interest.py +183 -0
- package/python/biocore_model/tests/test_self_layer_wiring.py +198 -0
- package/python/biocore_model/tests/test_self_verification.py +113 -0
- package/python/biocore_model/tests/test_situation.py +142 -0
- package/python/biocore_model/tests/test_sleep_circadian.py +41 -0
- package/python/biocore_model/tests/test_social_pain.py +49 -0
- package/python/biocore_model/tests/test_social_repair.py +67 -0
- package/python/biocore_model/tests/test_social_theory.py +78 -0
- package/python/biocore_model/tests/test_source_monitoring.py +204 -0
- package/python/biocore_model/tests/test_speak_like_group.py +36 -0
- package/python/biocore_model/tests/test_stages.py +226 -0
- package/python/biocore_model/tests/test_state_tag.py +86 -0
- package/python/biocore_model/tests/test_template_expr.py +65 -0
- package/python/biocore_model/tests/test_text_inference.py +70 -0
- package/python/biocore_model/tests/test_theta_params.py +115 -0
- package/python/biocore_model/tests/test_topic_control.py +200 -0
- package/python/biocore_model/tests/test_turn_memory.py +105 -0
- package/python/biocore_model/tests/test_turn_memory_panel.py +174 -0
- package/python/biocore_model/tests/test_ui_settings.py +84 -0
- package/python/biocore_model/tests/test_ui_settings_authority.py +179 -0
- package/python/biocore_model/tests/test_user_appraisal_async.py +155 -0
- package/python/biocore_model/tests/test_user_classify.py +122 -0
- package/python/biocore_model/tests/test_value_order.py +117 -0
- package/python/biocore_model/tests/test_voice_bank.py +105 -0
- package/python/biocore_model/tests/test_waiting_sense.py +216 -0
- package/python/biocore_model/tests/test_x_directives.py +101 -0
- package/python/biocore_model/tests/ui_compat_check.mjs +22 -0
- package/python/biocore_model/tests/view_mode_check.mjs +17 -0
- package/python/biocore_runtime/autonomy.py +208 -0
- package/python/biocore_runtime/core_state.py +302 -0
- package/python/biocore_runtime/governance.py +495 -0
- package/python/biocore_runtime/growth.py +528 -0
- package/python/biocore_runtime/pipeline.py +850 -23
- package/python/biocore_runtime/relationship_state.py +214 -0
- package/python/biocore_runtime/self_layer.py +103 -0
- package/python/biocore_runtime/service.py +162 -0
- package/python/biocore_runtime/standby.py +472 -0
- package/ui/panel-layout.js +83 -39
- package/ui/panel.css +885 -743
- package/ui/panel.html +356 -296
- package/ui/panel.js +739 -865
package/README.md
CHANGED
|
@@ -49,6 +49,17 @@ DSH `session/event` hook 每轮调用一次 `one_turn`,另有 `/bio sync <一
|
|
|
49
49
|
2) **兜底命令 `/bio sync <一句话>`**:给没有 hook 的宿主/手动调试用,
|
|
50
50
|
调同一个 bridge 更新当前会话,返回一行状态变化摘要(不产生模型消息)。
|
|
51
51
|
|
|
52
|
+
## 子智能体(delegationDepth >= 1)
|
|
53
|
+
|
|
54
|
+
core-turn 在组装 -190 注入前读取 session.header.delegationDepth(缺失按 0):
|
|
55
|
+
- depth = 0 / 缺失:保持现行为,注入人设、状态块与思考规则。
|
|
56
|
+
- depth >= 1:仍调用 turn_start 维持状态/记忆/trace,但把本会话的 prompt_block
|
|
57
|
+
置空,跳过人设/状态/思考规则注入;turn_end 的 gate/trace 照常。
|
|
58
|
+
|
|
59
|
+
persona preset 的常驻注入是宿主级(agent preset),插件无法按会话单独关掉;
|
|
60
|
+
本插件只保证 turn 注入对子智能体跳过。若 preset 仍向子智能体注入,需要在
|
|
61
|
+
宿主侧为 subagent 使用不含 persona 的 preset。
|
|
62
|
+
|
|
52
63
|
## 接口
|
|
53
64
|
|
|
54
65
|
`GET /api/biocore/state?session=<id>`
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/** task-223: one persistent python core per DSH plugin lifetime.
|
|
2
|
+
*
|
|
3
|
+
* start(dataDir) spawns python/biocore_runtime/service.py; call(action, payload)
|
|
4
|
+
* correlates requests by id; a crash restarts at most 3 times (0.5s/1s/2s) and
|
|
5
|
+
* every failure is logged as '__bioWorker ...'. stop() sends SIGTERM and
|
|
6
|
+
* force-kills after 5s. When the worker is down, callers fall back to the old
|
|
7
|
+
* spawn-per-call bridge (handled in py-bridge.js).
|
|
8
|
+
*/
|
|
9
|
+
import { spawn } from 'node:child_process'
|
|
10
|
+
import { fileURLToPath } from 'node:url'
|
|
11
|
+
import { join } from 'node:path'
|
|
12
|
+
import { embeddingEnv } from './modules/python-env.js'
|
|
13
|
+
|
|
14
|
+
export const PACKAGE_ROOT = fileURLToPath(new URL('..', import.meta.url))
|
|
15
|
+
export const PYTHON_DIR = join(PACKAGE_ROOT, 'python')
|
|
16
|
+
export const SERVICE_SCRIPT = join(PYTHON_DIR, 'biocore_runtime', 'service.py')
|
|
17
|
+
export const ADAPTER_DIR = join(PYTHON_DIR, 'dsh_adapter')
|
|
18
|
+
export const WORKER_LOG_PREFIX = '__bioWorker'
|
|
19
|
+
export const DEFAULT_TIMEOUT_MS = 10000
|
|
20
|
+
export const MAX_RESTARTS = 3
|
|
21
|
+
export const BACKOFF_MS = Object.freeze([500, 1000, 2000])
|
|
22
|
+
export const STOP_GRACE_MS = 5000
|
|
23
|
+
|
|
24
|
+
let worker = null
|
|
25
|
+
let exitHookInstalled = false
|
|
26
|
+
|
|
27
|
+
function log(message) {
|
|
28
|
+
try { console.error(WORKER_LOG_PREFIX + ' ' + message) } catch { /* ignore */ }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class WorkerUnavailable extends Error {
|
|
32
|
+
constructor(reason, detail) {
|
|
33
|
+
super(String(reason || 'worker_unavailable'))
|
|
34
|
+
this.name = 'WorkerUnavailable'
|
|
35
|
+
this.reason = String(reason || 'worker_unavailable')
|
|
36
|
+
this.detail = detail ? String(detail) : ''
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function pythonCommand() {
|
|
41
|
+
return process.env.BIOCORE_PYTHON || 'python'
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function workerEnv(dataDir) {
|
|
45
|
+
let embedding = {}
|
|
46
|
+
try { embedding = dataDir ? embeddingEnv(dataDir) : {} } catch { embedding = {} }
|
|
47
|
+
return Object.assign({}, process.env, embedding, {
|
|
48
|
+
BIOCORE_PLUGIN_DIR: PYTHON_DIR,
|
|
49
|
+
BIOCORE_ADAPTER_DIR: ADAPTER_DIR,
|
|
50
|
+
PYTHONIOENCODING: 'utf-8',
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isAlive(handle) {
|
|
55
|
+
return Boolean(handle && handle.child && handle.child.exitCode === null
|
|
56
|
+
&& handle.child.signalCode === null && !handle.stopping)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function rejectPending(handle, reason, detail) {
|
|
60
|
+
if (!handle || !handle.pending) return
|
|
61
|
+
for (const [id, entry] of handle.pending) {
|
|
62
|
+
clearTimeout(entry.timer)
|
|
63
|
+
entry.reject(new WorkerUnavailable(reason, detail))
|
|
64
|
+
handle.pending.delete(id)
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function handleLine(handle, line) {
|
|
69
|
+
const text = String(line || '').trim()
|
|
70
|
+
if (!text) return
|
|
71
|
+
let message = null
|
|
72
|
+
try { message = JSON.parse(text) } catch {
|
|
73
|
+
log('bad-json ' + text.slice(0, 120))
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
if (!message || typeof message !== 'object') return
|
|
77
|
+
const id = message.id == null ? '' : String(message.id)
|
|
78
|
+
const entry = handle.pending.get(id)
|
|
79
|
+
if (!entry) return
|
|
80
|
+
handle.pending.delete(id)
|
|
81
|
+
clearTimeout(entry.timer)
|
|
82
|
+
if (message.ok === true) entry.resolve(message.result === undefined ? {} : message.result)
|
|
83
|
+
else entry.reject(new WorkerUnavailable(message.reason || 'service_error',
|
|
84
|
+
message.detail || ''))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function attach(handle) {
|
|
88
|
+
const child = handle.child
|
|
89
|
+
let buffer = ''
|
|
90
|
+
child.stdout.on('data', (chunk) => {
|
|
91
|
+
buffer += String(chunk)
|
|
92
|
+
let index = buffer.indexOf('\n')
|
|
93
|
+
while (index >= 0) {
|
|
94
|
+
const line = buffer.slice(0, index)
|
|
95
|
+
buffer = buffer.slice(index + 1)
|
|
96
|
+
handleLine(handle, line)
|
|
97
|
+
index = buffer.indexOf('\n')
|
|
98
|
+
}
|
|
99
|
+
if (buffer.length > 1024 * 1024) buffer = ''
|
|
100
|
+
})
|
|
101
|
+
child.stderr.on('data', (chunk) => {
|
|
102
|
+
const text = String(chunk).trim()
|
|
103
|
+
if (text) log('stderr ' + text.split('\n').slice(-3).join(' | ').slice(0, 500))
|
|
104
|
+
})
|
|
105
|
+
child.on('error', (error) => {
|
|
106
|
+
log('error ' + String(error && error.message || error).slice(0, 200))
|
|
107
|
+
onGone(handle, 'spawn_error', String(error && error.message || error))
|
|
108
|
+
})
|
|
109
|
+
// A sandbox may refuse piped stdio (EPIPE on write). Swallow the stream error
|
|
110
|
+
// so the host process survives; callers fall back to spawn-per-call.
|
|
111
|
+
try { child.stdin.on('error', (error) => onGone(handle, 'stdin_error', String(error && error.message || error))) } catch { /* noop */ }
|
|
112
|
+
try { child.stdout.on('error', (error) => onGone(handle, 'stdout_error', String(error && error.message || error))) } catch { /* noop */ }
|
|
113
|
+
child.on('exit', (code, signal) => {
|
|
114
|
+
onGone(handle, 'exit', 'code=' + code + ' signal=' + signal)
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function onGone(handle, reason, detail) {
|
|
119
|
+
if (handle.gone) return
|
|
120
|
+
handle.gone = true
|
|
121
|
+
if (handle.stopping) {
|
|
122
|
+
rejectPending(handle, 'worker_stopped', '')
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
log('down reason=' + reason + ' ' + String(detail || '').slice(0, 160))
|
|
126
|
+
rejectPending(handle, 'worker_crashed', reason + ' ' + String(detail || ''))
|
|
127
|
+
if (handle.restarts >= MAX_RESTARTS) {
|
|
128
|
+
log('giving_up restarts=' + handle.restarts)
|
|
129
|
+
return
|
|
130
|
+
}
|
|
131
|
+
const delay = BACKOFF_MS[Math.min(handle.restarts, BACKOFF_MS.length - 1)]
|
|
132
|
+
handle.restarts += 1
|
|
133
|
+
log('restart=' + handle.restarts + ' delay_ms=' + delay)
|
|
134
|
+
handle.restartTimer = setTimeout(() => {
|
|
135
|
+
handle.restartTimer = null
|
|
136
|
+
if (handle.stopping || worker !== handle) return
|
|
137
|
+
spawnChild(handle)
|
|
138
|
+
}, delay)
|
|
139
|
+
if (typeof handle.restartTimer.unref === 'function') handle.restartTimer.unref()
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function spawnChild(handle) {
|
|
143
|
+
handle.gone = false
|
|
144
|
+
let child = null
|
|
145
|
+
try {
|
|
146
|
+
child = spawn(pythonCommand(), [SERVICE_SCRIPT], {
|
|
147
|
+
cwd: ADAPTER_DIR,
|
|
148
|
+
env: workerEnv(handle.dataDir),
|
|
149
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
150
|
+
windowsHide: true,
|
|
151
|
+
})
|
|
152
|
+
} catch (error) {
|
|
153
|
+
log('spawn_failed ' + String(error && error.message || error))
|
|
154
|
+
handle.child = null
|
|
155
|
+
onGone(handle, 'spawn_failed', String(error && error.message || error))
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
handle.child = child
|
|
159
|
+
attach(handle)
|
|
160
|
+
log('started pid=' + (child.pid || '?') + ' dataDir=' + String(handle.dataDir || ''))
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** Start (or reuse) the singleton worker. Idempotent while it is alive. */
|
|
164
|
+
export function startCoreWorker(dataDir) {
|
|
165
|
+
const dir = String(dataDir || '')
|
|
166
|
+
if (worker && isAlive(worker) && worker.dataDir === dir) return workerApi(worker)
|
|
167
|
+
if (worker) stopCoreWorker()
|
|
168
|
+
worker = {
|
|
169
|
+
dataDir: dir,
|
|
170
|
+
child: null,
|
|
171
|
+
pending: new Map(),
|
|
172
|
+
seq: 0,
|
|
173
|
+
restarts: 0,
|
|
174
|
+
stopping: false,
|
|
175
|
+
gone: false,
|
|
176
|
+
restartTimer: null,
|
|
177
|
+
startedAt: Date.now(),
|
|
178
|
+
}
|
|
179
|
+
spawnChild(worker)
|
|
180
|
+
if (!exitHookInstalled) {
|
|
181
|
+
exitHookInstalled = true
|
|
182
|
+
try {
|
|
183
|
+
process.once('exit', () => {
|
|
184
|
+
try { if (worker && worker.child) worker.child.kill() } catch { /* ignore */ }
|
|
185
|
+
})
|
|
186
|
+
} catch { /* ignore */ }
|
|
187
|
+
}
|
|
188
|
+
return workerApi(worker)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function isWorkerRunning() {
|
|
192
|
+
return isAlive(worker)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function workerPid() {
|
|
196
|
+
return worker && worker.child ? worker.child.pid : null
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function callCoreWorker(action, payload, options) {
|
|
200
|
+
const opts = options || {}
|
|
201
|
+
const timeoutMs = Number.isFinite(Number(opts.timeoutMs))
|
|
202
|
+
? Math.max(1, Number(opts.timeoutMs)) : DEFAULT_TIMEOUT_MS
|
|
203
|
+
return new Promise((resolve, reject) => {
|
|
204
|
+
if (!isAlive(worker)) {
|
|
205
|
+
reject(new WorkerUnavailable('worker_down', ''))
|
|
206
|
+
return
|
|
207
|
+
}
|
|
208
|
+
const id = 'w' + (++worker.seq)
|
|
209
|
+
const entry = { resolve, reject, timer: null }
|
|
210
|
+
entry.timer = setTimeout(() => {
|
|
211
|
+
worker.pending.delete(id)
|
|
212
|
+
reject(new WorkerUnavailable('timeout', 'action=' + action))
|
|
213
|
+
}, timeoutMs)
|
|
214
|
+
if (typeof entry.timer.unref === 'function') entry.timer.unref()
|
|
215
|
+
worker.pending.set(id, entry)
|
|
216
|
+
const line = JSON.stringify({
|
|
217
|
+
id,
|
|
218
|
+
action: String(action || ''),
|
|
219
|
+
payload: payload && typeof payload === 'object' ? payload : {},
|
|
220
|
+
}) + '\n'
|
|
221
|
+
try {
|
|
222
|
+
worker.child.stdin.write(line)
|
|
223
|
+
} catch (error) {
|
|
224
|
+
worker.pending.delete(id)
|
|
225
|
+
clearTimeout(entry.timer)
|
|
226
|
+
reject(new WorkerUnavailable('write_failed', String(error && error.message || error)))
|
|
227
|
+
}
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Stop the worker: SIGTERM, then force-kill after 5s. Never throws. */
|
|
232
|
+
export function stopCoreWorker() {
|
|
233
|
+
const handle = worker
|
|
234
|
+
if (!handle) return
|
|
235
|
+
handle.stopping = true
|
|
236
|
+
if (handle.restartTimer) {
|
|
237
|
+
clearTimeout(handle.restartTimer)
|
|
238
|
+
handle.restartTimer = null
|
|
239
|
+
}
|
|
240
|
+
rejectPending(handle, 'worker_stopped', '')
|
|
241
|
+
const child = handle.child
|
|
242
|
+
worker = null
|
|
243
|
+
if (!child) return
|
|
244
|
+
try { child.kill('SIGTERM') } catch { /* ignore */ }
|
|
245
|
+
const timer = setTimeout(() => {
|
|
246
|
+
try { if (child.exitCode === null && child.signalCode === null) child.kill('SIGKILL') } catch { /* ignore */ }
|
|
247
|
+
}, STOP_GRACE_MS)
|
|
248
|
+
if (typeof timer.unref === 'function') timer.unref()
|
|
249
|
+
child.once('exit', () => clearTimeout(timer))
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Test seam: kill the current process without marking it stopped (crash). */
|
|
253
|
+
export function killWorkerForTest(signal) {
|
|
254
|
+
if (!isAlive(worker)) return false
|
|
255
|
+
try { worker.child.kill(signal || 'SIGKILL') } catch { return false }
|
|
256
|
+
return true
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function workerApi(handle) {
|
|
260
|
+
return {
|
|
261
|
+
start: () => startCoreWorker(handle.dataDir),
|
|
262
|
+
call: (action, payload, options) => callCoreWorker(action, payload, options),
|
|
263
|
+
stop: () => stopCoreWorker(),
|
|
264
|
+
get pid() { return handle.child ? handle.child.pid : null },
|
|
265
|
+
get running() { return isAlive(handle) },
|
|
266
|
+
get restarts() { return handle.restarts },
|
|
267
|
+
}
|
|
268
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -17,22 +17,23 @@ import { dirname, join } from 'node:path'
|
|
|
17
17
|
import {
|
|
18
18
|
cleanSessionId,
|
|
19
19
|
payloadWithinLimit,
|
|
20
|
-
projectState,
|
|
21
|
-
readStateFile,
|
|
22
20
|
sessionStateFile,
|
|
23
21
|
STATE_LIMIT,
|
|
24
22
|
} from './modules/state.js'
|
|
25
|
-
import { compilePersona, ensureState, runTurn, runtimeAction } from './py-bridge.js'
|
|
23
|
+
import { compilePersona, configureBridge, configureCore, ensureState, memoryTool, runTurn, runtimeAction } from './py-bridge.js'
|
|
24
|
+
import { rerankConfig } from './modules/python-env.js'
|
|
26
25
|
import { SWITCH_ROUTE_PATH, handleSwitchRoute, readSwitches } from './modules/switch.js'
|
|
27
26
|
import { TRACE_ROUTE_PATH, defaultStoreDir, handleTraceRoute } from './modules/trace.js'
|
|
27
|
+
import { SERIES_ROUTE_PATH, handleSeriesRoute } from './modules/series.js'
|
|
28
|
+
import { readCacheStats } from './modules/cache-stats.js'
|
|
29
|
+
import { startCoreWorker, stopCoreWorker } from './core-worker.js'
|
|
30
|
+
import { runAutonomyTick } from './modules/autonomy.js'
|
|
28
31
|
import { queueSessionTurn } from './modules/turn-queue.js'
|
|
29
|
-
import { anySwitchOn } from './modules/expression-inject.js'
|
|
30
32
|
import { makeCoreSessionListener, registerCoreTurn } from './modules/core-turn.js'
|
|
31
33
|
import { migrateLegacyPluginData, PLUGIN_DATA_DIR, resolveDataDir } from './modules/data-dir.js'
|
|
32
34
|
import {
|
|
33
35
|
PANEL_MOUNT_PATH,
|
|
34
36
|
PANEL_ROUTE_PREFIX,
|
|
35
|
-
buildPanelExtras,
|
|
36
37
|
servePanelPage,
|
|
37
38
|
} from './modules/panel-data.js'
|
|
38
39
|
import { resolveMemoryDir } from './modules/memory-store.js'
|
|
@@ -91,7 +92,7 @@ export function settingsPaths(dataDir) {
|
|
|
91
92
|
export const EMBEDDING_MODES = Object.freeze(['auto', 'remote', 'local', 'off'])
|
|
92
93
|
|
|
93
94
|
function emptyEmbedding() {
|
|
94
|
-
return { mode: 'auto', base_url: '', model: '', api_key: ''
|
|
95
|
+
return { mode: 'auto', base_url: '', model: '', api_key: '' }
|
|
95
96
|
}
|
|
96
97
|
|
|
97
98
|
function normalizeEmbedding(value) {
|
|
@@ -102,11 +103,35 @@ function normalizeEmbedding(value) {
|
|
|
102
103
|
base_url: String(source.base_url || '').trim().slice(0, 2000),
|
|
103
104
|
model: String(source.model || '').trim().slice(0, 200),
|
|
104
105
|
api_key: typeof source.api_key === 'string' ? source.api_key.trim().slice(0, 4000) : '',
|
|
105
|
-
local_path: String(source.local_path || '').trim().slice(0, 2000),
|
|
106
106
|
}
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
export const
|
|
109
|
+
export const AUTONOMY_DEFAULTS = Object.freeze({
|
|
110
|
+
max_actions_per_hour: 20,
|
|
111
|
+
max_sends_per_hour: 5,
|
|
112
|
+
max_sends_per_day: 30,
|
|
113
|
+
quiet_hours: [],
|
|
114
|
+
auto_decay_on_silence: true,
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
function normalizeAutonomy(value) {
|
|
118
|
+
const source = value && typeof value === 'object' && !Array.isArray(value)
|
|
119
|
+
? value : {}
|
|
120
|
+
const int = (raw, fallback) => {
|
|
121
|
+
const number = Number(raw)
|
|
122
|
+
return Number.isFinite(number) ? Math.max(0, Math.round(number)) : fallback
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
max_actions_per_hour: int(source.max_actions_per_hour, AUTONOMY_DEFAULTS.max_actions_per_hour),
|
|
126
|
+
max_sends_per_hour: int(source.max_sends_per_hour, AUTONOMY_DEFAULTS.max_sends_per_hour),
|
|
127
|
+
max_sends_per_day: int(source.max_sends_per_day, AUTONOMY_DEFAULTS.max_sends_per_day),
|
|
128
|
+
quiet_hours: Array.isArray(source.quiet_hours)
|
|
129
|
+
? source.quiet_hours.map((item) => String(item).slice(0, 12)).slice(0, 12) : [],
|
|
130
|
+
auto_decay_on_silence: source.auto_decay_on_silence !== false,
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const RERANK_DEFAULTS = Object.freeze({
|
|
110
135
|
enabled: false,
|
|
111
136
|
base_url: 'https://api.jina.ai/v1',
|
|
112
137
|
model: 'jina-reranker-v2-base-multilingual',
|
|
@@ -167,6 +192,7 @@ export function readFullConfig(dataDir) {
|
|
|
167
192
|
embedding: storedEmbedding(source),
|
|
168
193
|
rerank: normalizeRerank(source.rerank),
|
|
169
194
|
expression: normalizeExpression(source.expression),
|
|
195
|
+
autonomy: normalizeAutonomy(source.autonomy),
|
|
170
196
|
sanitize_reply: source.sanitize_reply !== false,
|
|
171
197
|
}
|
|
172
198
|
}
|
|
@@ -177,6 +203,7 @@ function writeFullConfig(file, config) {
|
|
|
177
203
|
embedding: config.embedding,
|
|
178
204
|
rerank: config.rerank,
|
|
179
205
|
expression: config.expression,
|
|
206
|
+
autonomy: config.autonomy,
|
|
180
207
|
sanitize_reply: config.sanitize_reply !== false,
|
|
181
208
|
}, null, 2) + '\n', { encoding: 'utf8', mode: 0o600 })
|
|
182
209
|
try { chmodSync(file, 0o600) } catch { /* Windows may ignore POSIX modes */ }
|
|
@@ -193,6 +220,7 @@ export async function handleSettings(req, res, dataDir) {
|
|
|
193
220
|
embedding: maskedEmbedding(config.embedding),
|
|
194
221
|
rerank: maskedRerank(config.rerank),
|
|
195
222
|
expression: config.expression,
|
|
223
|
+
autonomy: config.autonomy,
|
|
196
224
|
sanitize_reply: config.sanitize_reply !== false,
|
|
197
225
|
restart_required: false,
|
|
198
226
|
})
|
|
@@ -217,7 +245,9 @@ export async function handleSettings(req, res, dataDir) {
|
|
|
217
245
|
const hasRerank = body.rerank && typeof body.rerank === 'object'
|
|
218
246
|
const hasExpression = body.expression && typeof body.expression === 'object'
|
|
219
247
|
const hasSanitize = typeof body.sanitize_reply === 'boolean'
|
|
220
|
-
|
|
248
|
+
const hasAutonomy = body.autonomy && typeof body.autonomy === 'object'
|
|
249
|
+
if (!hasEmbedding && !hasRerank && !hasExpression && !hasSanitize
|
|
250
|
+
&& !hasAutonomy) {
|
|
221
251
|
return sendJson(res, 400, { ok: false, reason: 'no_settings' })
|
|
222
252
|
}
|
|
223
253
|
if (hasEmbedding) {
|
|
@@ -240,13 +270,16 @@ export async function handleSettings(req, res, dataDir) {
|
|
|
240
270
|
config.rerank = rerank
|
|
241
271
|
}
|
|
242
272
|
if (hasExpression) config.expression = normalizeExpression(body.expression)
|
|
273
|
+
if (hasAutonomy) config.autonomy = normalizeAutonomy(body.autonomy)
|
|
243
274
|
if (hasSanitize) config.sanitize_reply = body.sanitize_reply === true
|
|
244
275
|
writeFullConfig(paths.file, config)
|
|
276
|
+
void configureCoreRuntime(paths.dir)
|
|
245
277
|
return sendJson(res, 200, {
|
|
246
278
|
ok: true,
|
|
247
279
|
embedding: maskedEmbedding(config.embedding),
|
|
248
280
|
rerank: maskedRerank(config.rerank),
|
|
249
281
|
expression: config.expression,
|
|
282
|
+
autonomy: config.autonomy,
|
|
250
283
|
sanitize_reply: config.sanitize_reply !== false,
|
|
251
284
|
restart_required: true,
|
|
252
285
|
hint: '重启 BioCore 生效',
|
|
@@ -342,6 +375,30 @@ export async function handleSettingsTest(req, res, options) {
|
|
|
342
375
|
return sendJson(res, 200, result)
|
|
343
376
|
}
|
|
344
377
|
|
|
378
|
+
/** task-223: the adapter only ships the config; the core decides everything. */
|
|
379
|
+
function coreConfigPayload(dataDir) {
|
|
380
|
+
const config = readFullConfig(dataDir)
|
|
381
|
+
let switches = {}
|
|
382
|
+
try { switches = readSwitches(dataDir).switches } catch { switches = {} }
|
|
383
|
+
let persona = ''
|
|
384
|
+
try { persona = readPersonaText(dataDir) } catch { persona = '' }
|
|
385
|
+
return {
|
|
386
|
+
data_dir: dataDir,
|
|
387
|
+
persona_text: persona,
|
|
388
|
+
embedding: config.embedding,
|
|
389
|
+
rerank: config.rerank,
|
|
390
|
+
expression: config.expression,
|
|
391
|
+
autonomy: config.autonomy,
|
|
392
|
+
sanitize_reply: config.sanitize_reply !== false,
|
|
393
|
+
switches,
|
|
394
|
+
recall_mode: 'query',
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async function configureCoreRuntime(dataDir) {
|
|
399
|
+
try { await configureCore(coreConfigPayload(dataDir)) } catch { /* core decides defaults */ }
|
|
400
|
+
}
|
|
401
|
+
|
|
345
402
|
const PERSONA_PREVIEW_FIELDS = [
|
|
346
403
|
['E', '外向', 2],
|
|
347
404
|
['A', '宜人', 2],
|
|
@@ -686,32 +743,31 @@ export async function fetchCoreState(sessionId, options) {
|
|
|
686
743
|
return value
|
|
687
744
|
}
|
|
688
745
|
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
const
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
const intero = (core.intero && typeof core.intero === 'object')
|
|
698
|
-
? core.intero
|
|
699
|
-
: ((core.core && core.core.intero && typeof core.core.intero === 'object') ? core.core.intero : null)
|
|
700
|
-
if (intero) base.intero = Object.assign({}, base.intero || {}, intero)
|
|
701
|
-
if (core.relationship && typeof core.relationship === 'object') {
|
|
702
|
-
const rel = core.relationship
|
|
703
|
-
const metrics = Object.assign({}, base.relationship_metrics || {})
|
|
704
|
-
for (const key of ['closeness', 'trust', 'conflict', 'bids']) {
|
|
705
|
-
const value = Number(rel[key])
|
|
706
|
-
if (Number.isFinite(value)) metrics[key] = Math.round(value * 100) / 100
|
|
707
|
-
}
|
|
708
|
-
base.relationship_metrics = metrics
|
|
709
|
-
}
|
|
710
|
-
for (const key of ['v', 'intero', 'tone', 'relation', 'autonomy', 'history',
|
|
711
|
-
'memory_recent', 'narrative', 'body_metrics', 'motivation']) {
|
|
712
|
-
if (core[key] !== undefined) base[key] = core[key]
|
|
746
|
+
export const STATS_ROUTE_PATH = '/api/biocore/stats'
|
|
747
|
+
|
|
748
|
+
/** task-223: /stats is a pure forward of the core reply (+ display-only cache). */
|
|
749
|
+
export async function handleStats(req, res, options) {
|
|
750
|
+
const opts = options || {}
|
|
751
|
+
const method = String((req && req.method) || 'GET').toUpperCase()
|
|
752
|
+
if (method !== 'GET' && method !== 'HEAD') {
|
|
753
|
+
return sendJson(res, 405, { ok: false, reason: 'method_not_allowed' })
|
|
713
754
|
}
|
|
714
|
-
|
|
755
|
+
let reply = null
|
|
756
|
+
try {
|
|
757
|
+
const runAction = typeof opts.runAction === 'function' ? opts.runAction : runtimeAction
|
|
758
|
+
const store = (() => {
|
|
759
|
+
try { return typeof opts.getStore === 'function' ? opts.getStore() : '' } catch { return '' }
|
|
760
|
+
})()
|
|
761
|
+
reply = await runAction('stats', { store })
|
|
762
|
+
} catch { reply = null }
|
|
763
|
+
const body = Object.assign({ ok: false, reason: 'core_unavailable' },
|
|
764
|
+
reply && typeof reply === 'object' ? reply : {})
|
|
765
|
+
try {
|
|
766
|
+
if (typeof opts.dataDir === 'string' && opts.dataDir) {
|
|
767
|
+
body.cache = readCacheStats(opts.dataDir)
|
|
768
|
+
}
|
|
769
|
+
} catch { /* display only */ }
|
|
770
|
+
return sendJson(res, 200, body)
|
|
715
771
|
}
|
|
716
772
|
|
|
717
773
|
export async function handleState(req, res, stateDir, options) {
|
|
@@ -725,43 +781,29 @@ export async function handleState(req, res, stateDir, options) {
|
|
|
725
781
|
if (!rawSession) return sendJson(res, 200, unavailable('no_session'))
|
|
726
782
|
const sessionId = cleanSessionId(rawSession)
|
|
727
783
|
if (!sessionId) return sendJson(res, 200, unavailable('bad_session'))
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
core = await fetchCoreState(sessionId, {
|
|
739
|
-
runAction: opts.runAction,
|
|
740
|
-
store: opts.store,
|
|
741
|
-
})
|
|
742
|
-
}
|
|
743
|
-
} catch { core = null }
|
|
744
|
-
if (!core || core.ok !== true || !core.state || typeof core.state !== 'object') {
|
|
745
|
-
return sendJson(res, 200, unavailable('no_state_yet'))
|
|
746
|
-
}
|
|
747
|
-
body = projectState(core.state, { mtimeMs: 0, sessionId })
|
|
748
|
-
Object.assign(body, buildPanelExtras(core.state))
|
|
749
|
-
body.source = 'runtime'
|
|
750
|
-
} else if (read.error) {
|
|
751
|
-
return sendJson(res, 200, unavailable(read.error))
|
|
752
|
-
} else {
|
|
753
|
-
body = projectState(read.state, { mtimeMs: read.mtimeMs, sessionId })
|
|
754
|
-
Object.assign(body, buildPanelExtras(read.state))
|
|
755
|
-
body.source = 'file'
|
|
756
|
-
}
|
|
757
|
-
if (!payloadWithinLimit(body)) {
|
|
784
|
+
// task-223: pure forward -- the core owns the whole /state payload.
|
|
785
|
+
let reply = null
|
|
786
|
+
try {
|
|
787
|
+
const runAction = typeof opts.runAction === 'function' ? opts.runAction : runtimeAction
|
|
788
|
+
reply = await runAction('state', { session_id: sessionId, store: opts.store })
|
|
789
|
+
} catch { reply = null }
|
|
790
|
+
const state = reply && reply.ok === true && reply.state
|
|
791
|
+
&& typeof reply.state === 'object' ? reply.state : null
|
|
792
|
+
if (!state) return sendJson(res, 200, unavailable('core_unavailable'))
|
|
793
|
+
if (!payloadWithinLimit(state)) {
|
|
758
794
|
return sendJson(res, 200, unavailable('payload_too_large'))
|
|
759
795
|
}
|
|
760
|
-
return sendJson(res, 200,
|
|
796
|
+
return sendJson(res, 200, state)
|
|
761
797
|
}
|
|
762
798
|
|
|
763
799
|
export function apply(ctx, config) {
|
|
764
800
|
const dataDir = resolveDataDir(config)
|
|
801
|
+
configureBridge(dataDir)
|
|
802
|
+
// task-223: one persistent core process for this plugin lifetime.
|
|
803
|
+
try { startCoreWorker(dataDir) } catch (error) {
|
|
804
|
+
try { console.error('__bioWorker start_failed ' + String(error)) } catch { /* ignore */ }
|
|
805
|
+
}
|
|
806
|
+
void configureCoreRuntime(dataDir)
|
|
765
807
|
try { migrateLegacyPluginData(dataDir) } catch { /* best-effort data migration */ }
|
|
766
808
|
try { migratePersonaToShared(dataDir) } catch { /* best-effort migration */ }
|
|
767
809
|
const disposers = []
|
|
@@ -799,14 +841,36 @@ export function apply(ctx, config) {
|
|
|
799
841
|
path: STATE_ROUTE_PATH,
|
|
800
842
|
handler: (req, res) => handleState(req, res, dataDir, { getSwitches, store: resolveMemoryDir(dataDir) }),
|
|
801
843
|
}))
|
|
802
|
-
// task-
|
|
844
|
+
// task-221: 24-point mood/channel series (global core_state history).
|
|
803
845
|
keep(webServer.register({
|
|
804
846
|
kind: 'exact',
|
|
805
|
-
path:
|
|
806
|
-
handler: (req, res) =>
|
|
847
|
+
path: SERIES_ROUTE_PATH,
|
|
848
|
+
handler: (req, res) => handleSeriesRoute(req, res, {
|
|
849
|
+
store: resolveMemoryDir(dataDir), dataDir,
|
|
850
|
+
}),
|
|
851
|
+
}))
|
|
852
|
+
// embedding cache stats (action=stats; zeros when unavailable).
|
|
853
|
+
keep(webServer.register({
|
|
854
|
+
kind: 'exact',
|
|
855
|
+
path: STATS_ROUTE_PATH,
|
|
856
|
+
handler: (req, res) => handleStats(req, res, {
|
|
857
|
+
getSwitches,
|
|
858
|
+
getStore: () => resolveMemoryDir(dataDir),
|
|
807
859
|
dataDir,
|
|
808
860
|
}),
|
|
809
861
|
}))
|
|
862
|
+
// task-196: four switches persist to <dataDir>/flags.json (atomic write).
|
|
863
|
+
keep(webServer.register({
|
|
864
|
+
kind: 'exact',
|
|
865
|
+
path: SWITCH_ROUTE_PATH,
|
|
866
|
+
handler: async (req, res) => {
|
|
867
|
+
const out = await handleSwitchRoute(req, res, { dataDir })
|
|
868
|
+
if (String((req && req.method) || '').toUpperCase() === 'POST') {
|
|
869
|
+
void configureCoreRuntime(dataDir)
|
|
870
|
+
}
|
|
871
|
+
return out
|
|
872
|
+
},
|
|
873
|
+
}))
|
|
810
874
|
// task-210: read-only "why this turn" traces written by python.
|
|
811
875
|
keep(webServer.register({
|
|
812
876
|
kind: 'exact',
|
|
@@ -879,11 +943,16 @@ export function apply(ctx, config) {
|
|
|
879
943
|
} catch { /* memory capture unavailable */ }
|
|
880
944
|
try {
|
|
881
945
|
const injection = registerMemoryInjection(ctx, () => config, memoryDir,
|
|
882
|
-
injectionState
|
|
946
|
+
injectionState, {
|
|
947
|
+
getRerankConfig: () => rerankConfig(dataDir),
|
|
948
|
+
getSwitches,
|
|
949
|
+
})
|
|
883
950
|
if (injection) keep(injection)
|
|
884
951
|
} catch { /* prompt service unavailable */ }
|
|
885
952
|
try {
|
|
886
|
-
const tools = registerMemoryTools(ctx, () => config, memoryDir
|
|
953
|
+
const tools = registerMemoryTools(ctx, () => config, memoryDir, {
|
|
954
|
+
getRerankConfig: () => rerankConfig(dataDir),
|
|
955
|
+
})
|
|
887
956
|
if (tools) keep(tools.dispose)
|
|
888
957
|
} catch { /* tool service unavailable */ }
|
|
889
958
|
|
|
@@ -903,10 +972,31 @@ export function apply(ctx, config) {
|
|
|
903
972
|
}
|
|
904
973
|
} catch { /* headless / no host services */ }
|
|
905
974
|
|
|
975
|
+
// task-222: autonomy tick -- the core decides, this loop only executes.
|
|
976
|
+
if (!config || config.autonomyEnabled !== false) {
|
|
977
|
+
let timer = null
|
|
978
|
+
const tick = async () => {
|
|
979
|
+
try {
|
|
980
|
+
await runAutonomyTick(ctx, {
|
|
981
|
+
runAction: runtimeAction,
|
|
982
|
+
store: resolveMemoryDir(dataDir),
|
|
983
|
+
dataDir,
|
|
984
|
+
sessionId: 'autonomy',
|
|
985
|
+
memoryTool,
|
|
986
|
+
rerankConfig: rerankConfig(dataDir),
|
|
987
|
+
})
|
|
988
|
+
} catch { /* an autonomy tick never disturbs a turn */ }
|
|
989
|
+
}
|
|
990
|
+
timer = setInterval(() => { void tick() }, 5 * 60 * 1000)
|
|
991
|
+
if (typeof timer.unref === 'function') timer.unref()
|
|
992
|
+
keep(() => { if (timer !== null) { clearInterval(timer); timer = null } })
|
|
993
|
+
}
|
|
994
|
+
|
|
906
995
|
return () => {
|
|
907
996
|
for (const dispose of disposers) {
|
|
908
997
|
try { if (typeof dispose === 'function') dispose() } catch { /* ignore */ }
|
|
909
998
|
}
|
|
999
|
+
try { stopCoreWorker() } catch { /* ignore */ }
|
|
910
1000
|
}
|
|
911
1001
|
}
|
|
912
1002
|
|