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.
- 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
|
@@ -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
|
+
}
|
package/lib/modules/core-turn.js
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
+
}
|
package/lib/modules/switch.js
CHANGED
|
@@ -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). */
|