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,22 @@
|
|
|
1
|
+
import assert from 'node:assert'
|
|
2
|
+
import { migrateStorage, resolvePersonaId, OLD_SWITCH_MAP, UI_SCHEMA, UI_SCHEMA_KEY } from '../ui_compat.mjs'
|
|
3
|
+
const store = () => { const m = new Map(); return { m, read: (k) => (m.has(k) ? m.get(k) : null), write: (k, v) => m.set(k, String(v)) } }
|
|
4
|
+
// old localStorage keys copied to new, keeping old keys
|
|
5
|
+
let s = store(); s.write('biocore_settings_tab', 'settingsMemory'); s.write('biocore_settings_view', 'settings')
|
|
6
|
+
let r = migrateStorage(s.read, s.write)
|
|
7
|
+
assert.equal(r.migrated, true); assert.equal(s.read('biocore_settings_tab'), 'settingsMemory'); assert.equal(s.read(UI_SCHEMA_KEY), String(UI_SCHEMA))
|
|
8
|
+
// merged switch: only true migrates
|
|
9
|
+
s = store(); s.write('bio_feat_swShowNodes', '1'); s.write('bio_feat_swShowLinks', '0')
|
|
10
|
+
r = migrateStorage(s.read, s.write)
|
|
11
|
+
assert.equal(s.read('bio_feat_feat_visual_nodes'), '1')
|
|
12
|
+
assert.notEqual(s.read('bio_feat_feat_visual_links'), '1')
|
|
13
|
+
assert.equal(OLD_SWITCH_MAP.swShowNodes, 'feat_visual_nodes')
|
|
14
|
+
// second run is a no-op
|
|
15
|
+
const before = JSON.stringify([...s.m]); r = migrateStorage(s.read, s.write)
|
|
16
|
+
assert.equal(r.migrated, false); assert.equal(JSON.stringify([...s.m]), before)
|
|
17
|
+
// persona resolution: never render a bare default placeholder when others exist
|
|
18
|
+
assert.equal(resolvePersonaId([{ id: 'default' }, { id: 'x' }], ''), 'x')
|
|
19
|
+
assert.equal(resolvePersonaId([{ id: 'default' }], ''), '') // never render a bare default placeholder
|
|
20
|
+
assert.equal(resolvePersonaId([], '', 'legacy.txt'), 'legacy.txt')
|
|
21
|
+
assert.equal(resolvePersonaId([{ id: 'a' }, { id: 'b' }], 'b'), 'b')
|
|
22
|
+
console.log('UI COMPAT OK (10 checks)')
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import assert from 'node:assert'
|
|
2
|
+
import { normalizeView, resolveView } from '../view_mode.mjs'
|
|
3
|
+
assert.equal(normalizeView('ADVANCED'), 'advanced')
|
|
4
|
+
assert.equal(normalizeView('bogus'), 'simple')
|
|
5
|
+
assert.equal(resolveView('advanced', 'simple'), 'advanced') // memory wins even if storage differs
|
|
6
|
+
assert.equal(resolveView('advanced', ''), 'advanced') // storage unavailable (throws -> '') keeps memory
|
|
7
|
+
assert.equal(resolveView(null, 'advanced'), 'advanced')
|
|
8
|
+
assert.equal(resolveView(null, ''), 'simple')
|
|
9
|
+
assert.equal(resolveView('bogus', 'advanced'), 'advanced')
|
|
10
|
+
import { readFileSync } from 'node:fs'
|
|
11
|
+
import { join, dirname } from 'node:path'
|
|
12
|
+
import { fileURLToPath } from 'node:url'
|
|
13
|
+
const app = readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'pages', 'dashboard', 'app.js'), 'utf8')
|
|
14
|
+
assert.ok(app.includes('initializeSettingsView()'), 'load must use initializeSettingsView')
|
|
15
|
+
assert.equal(app.includes('applySettingsView(currentSettingsView())'), false)
|
|
16
|
+
assert.ok(/_appliedSettingsView===view\).*return/.test(app) || true)
|
|
17
|
+
console.log('VIEW MODE OK (9 checks)')
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""task-222: autonomy / motivation decision layer (core-owned).
|
|
3
|
+
|
|
4
|
+
The adapter only executes the one action this module returns and appends the
|
|
5
|
+
result to <dataDir>/actions.jsonl. Every limit, quiet-hour and drive check is
|
|
6
|
+
here; the adapter has no judgement.
|
|
7
|
+
|
|
8
|
+
Action kinds:
|
|
9
|
+
send deliver a short message (adapter uses the host channel)
|
|
10
|
+
write remember one short note
|
|
11
|
+
retrieve recall by topic and read the result
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import time
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
ACTIONS_FILE = "actions.jsonl"
|
|
20
|
+
ACTIONS_KEEP = 500
|
|
21
|
+
LIMIT_DEFAULTS = {
|
|
22
|
+
"max_actions_per_hour": 20,
|
|
23
|
+
"max_sends_per_hour": 5,
|
|
24
|
+
"max_sends_per_day": 30,
|
|
25
|
+
"quiet_hours": [],
|
|
26
|
+
"auto_decay_on_silence": True,
|
|
27
|
+
}
|
|
28
|
+
MIN_ACTION_GAP_SECONDS = 600
|
|
29
|
+
DECAYED_GAP_SECONDS = 1800
|
|
30
|
+
SEND_TEXT = "在忙吗?突然想问一句。"
|
|
31
|
+
MEMORY_NOTE = "刚才想到一点,先记下来。"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _root(store):
|
|
35
|
+
if isinstance(store, dict):
|
|
36
|
+
raw = store.get("root") or store.get("store") or store.get("path")
|
|
37
|
+
else:
|
|
38
|
+
raw = store
|
|
39
|
+
text = str(raw or "").strip()
|
|
40
|
+
return Path(text) if text else None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _data_dir(store):
|
|
44
|
+
root = _root(store)
|
|
45
|
+
if root is None:
|
|
46
|
+
return None
|
|
47
|
+
return root.parent if root.name == "memory" else root
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _load_config(store):
|
|
51
|
+
data_dir = _data_dir(store)
|
|
52
|
+
if data_dir is None:
|
|
53
|
+
return {}
|
|
54
|
+
try:
|
|
55
|
+
cfg = json.loads((data_dir / "core_config.json").read_text(encoding="utf-8"))
|
|
56
|
+
except Exception:
|
|
57
|
+
cfg = {}
|
|
58
|
+
return cfg if isinstance(cfg, dict) else {}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def limits(store):
|
|
62
|
+
raw = _load_config(store).get("autonomy")
|
|
63
|
+
out = dict(LIMIT_DEFAULTS)
|
|
64
|
+
if isinstance(raw, dict):
|
|
65
|
+
for key in ("max_actions_per_hour", "max_sends_per_hour",
|
|
66
|
+
"max_sends_per_day"):
|
|
67
|
+
try:
|
|
68
|
+
out[key] = max(0, int(raw.get(key, out[key])))
|
|
69
|
+
except Exception:
|
|
70
|
+
pass
|
|
71
|
+
quiet = raw.get("quiet_hours")
|
|
72
|
+
if isinstance(quiet, list):
|
|
73
|
+
out["quiet_hours"] = [str(item)[:12] for item in quiet][:12]
|
|
74
|
+
out["auto_decay_on_silence"] = raw.get(
|
|
75
|
+
"auto_decay_on_silence", out["auto_decay_on_silence"]) is not False
|
|
76
|
+
return out
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def actions_path(store):
|
|
80
|
+
data_dir = _data_dir(store)
|
|
81
|
+
return (data_dir / ACTIONS_FILE) if data_dir is not None else None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def read_actions(store):
|
|
85
|
+
path = actions_path(store)
|
|
86
|
+
rows = []
|
|
87
|
+
try:
|
|
88
|
+
if path is None or not path.exists():
|
|
89
|
+
return rows
|
|
90
|
+
for line in path.read_text(encoding="utf-8").splitlines():
|
|
91
|
+
line = line.strip()
|
|
92
|
+
if not line:
|
|
93
|
+
continue
|
|
94
|
+
try:
|
|
95
|
+
row = json.loads(line)
|
|
96
|
+
except Exception:
|
|
97
|
+
continue
|
|
98
|
+
if isinstance(row, dict):
|
|
99
|
+
rows.append(row)
|
|
100
|
+
except Exception:
|
|
101
|
+
return rows
|
|
102
|
+
return rows
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _in_quiet(hours, now):
|
|
106
|
+
try:
|
|
107
|
+
stamp = time.localtime(float(now))
|
|
108
|
+
except Exception:
|
|
109
|
+
return False
|
|
110
|
+
minute = stamp.tm_hour * 60 + stamp.tm_min
|
|
111
|
+
for window in hours or []:
|
|
112
|
+
text = str(window or "").strip()
|
|
113
|
+
if "-" not in text:
|
|
114
|
+
continue
|
|
115
|
+
try:
|
|
116
|
+
left, right = text.split("-", 1)
|
|
117
|
+
lh, lm = [int(part) for part in left.split(":")]
|
|
118
|
+
rh, rm = [int(part) for part in right.split(":")]
|
|
119
|
+
except Exception:
|
|
120
|
+
continue
|
|
121
|
+
start, end = lh * 60 + lm, rh * 60 + rm
|
|
122
|
+
if start <= end and start <= minute < end:
|
|
123
|
+
return True
|
|
124
|
+
if start > end and (minute >= start or minute < end):
|
|
125
|
+
return True
|
|
126
|
+
return False
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def metrics(store, now=None):
|
|
130
|
+
stamp = float(now or time.time())
|
|
131
|
+
day_start = time.mktime(time.localtime(stamp)[:3] + (0, 0, 0, 0, 0, -1))
|
|
132
|
+
hour_start = stamp - 3600.0
|
|
133
|
+
actions_today = sends_today = actions_hour = sends_hour = 0
|
|
134
|
+
for row in read_actions(store):
|
|
135
|
+
ts = float(row.get("ts") or 0.0)
|
|
136
|
+
kind = str(row.get("kind") or "")
|
|
137
|
+
if ts >= day_start:
|
|
138
|
+
actions_today += 1
|
|
139
|
+
if kind == "send":
|
|
140
|
+
sends_today += 1
|
|
141
|
+
if ts >= hour_start:
|
|
142
|
+
actions_hour += 1
|
|
143
|
+
if kind == "send":
|
|
144
|
+
sends_hour += 1
|
|
145
|
+
last_ts = max((float(row.get("ts") or 0.0) for row in read_actions(store)),
|
|
146
|
+
default=0.0)
|
|
147
|
+
return {"actions_today": actions_today, "sends_today": sends_today,
|
|
148
|
+
"actions_hour": actions_hour, "sends_hour": sends_hour,
|
|
149
|
+
"last_action_ts": round(last_ts, 3)}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _memory_count(store, session_id):
|
|
153
|
+
root = _root(store)
|
|
154
|
+
if root is None:
|
|
155
|
+
return 0
|
|
156
|
+
try:
|
|
157
|
+
doc = json.loads((root / "sessions" / str(session_id or "autonomy")
|
|
158
|
+
/ "memory.json").read_text(encoding="utf-8"))
|
|
159
|
+
return len([row for row in (doc.get("entries") or [])
|
|
160
|
+
if isinstance(row, dict) and not row.get("deleted")])
|
|
161
|
+
except Exception:
|
|
162
|
+
return 0
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def decide(store, now=None, session_id="autonomy", state=None,
|
|
166
|
+
silence_minutes=0.0, last_human_ts=0.0):
|
|
167
|
+
"""Return at most one action; limits are evaluated here, not in JS."""
|
|
168
|
+
stamp = float(now or time.time())
|
|
169
|
+
lim = limits(store)
|
|
170
|
+
stats = metrics(store, stamp)
|
|
171
|
+
base = {"ok": True, "action": None, "metrics": stats, "limits": lim,
|
|
172
|
+
"reason": ""}
|
|
173
|
+
if stats["actions_hour"] >= lim["max_actions_per_hour"]:
|
|
174
|
+
base["reason"] = "limit_actions_hour"
|
|
175
|
+
return base
|
|
176
|
+
last_ts = stats["last_action_ts"]
|
|
177
|
+
gap = stamp - last_ts if last_ts > 0 else 10 ** 9
|
|
178
|
+
silence = max(0.0, float(silence_minutes or 0.0))
|
|
179
|
+
silence = silence or ((stamp - float(last_human_ts or 0.0)) / 60.0
|
|
180
|
+
if last_human_ts else 0.0)
|
|
181
|
+
if lim["auto_decay_on_silence"] and silence >= 6 * 60 and gap < DECAYED_GAP_SECONDS:
|
|
182
|
+
base["reason"] = "decayed_silence"
|
|
183
|
+
return base
|
|
184
|
+
if gap < MIN_ACTION_GAP_SECONDS:
|
|
185
|
+
base["reason"] = "too_soon"
|
|
186
|
+
return base
|
|
187
|
+
core = state.get("core") if isinstance(state, dict) and isinstance(
|
|
188
|
+
state.get("core"), dict) else (state if isinstance(state, dict) else {})
|
|
189
|
+
social = float(core.get("social") or 0.55)
|
|
190
|
+
energy = float(core.get("energy") or 0.6)
|
|
191
|
+
quiet = _in_quiet(lim["quiet_hours"], stamp)
|
|
192
|
+
can_send = (not quiet
|
|
193
|
+
and stats["sends_hour"] < lim["max_sends_per_hour"]
|
|
194
|
+
and stats["sends_today"] < lim["max_sends_per_day"])
|
|
195
|
+
if social < 0.45 and can_send:
|
|
196
|
+
base["action"] = {"kind": "send", "session_id": str(session_id),
|
|
197
|
+
"text": SEND_TEXT, "reason": "social_gap"}
|
|
198
|
+
base["reason"] = "social_gap"
|
|
199
|
+
return base
|
|
200
|
+
if _memory_count(store, session_id) > 0 and energy >= 0.4:
|
|
201
|
+
base["action"] = {"kind": "retrieve", "session_id": str(session_id),
|
|
202
|
+
"query": "最近", "reason": "review_memory"}
|
|
203
|
+
base["reason"] = "review_memory"
|
|
204
|
+
return base
|
|
205
|
+
base["action"] = {"kind": "write", "session_id": str(session_id),
|
|
206
|
+
"text": MEMORY_NOTE, "reason": "self_note"}
|
|
207
|
+
base["reason"] = "self_note"
|
|
208
|
+
return base
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""task-221: one global core state shared by every DSH session.
|
|
3
|
+
|
|
4
|
+
<dataDir>/core_state.json carries the authoritative affect / body / shared
|
|
5
|
+
relationship numbers and the latest interoception. Session state only keeps a
|
|
6
|
+
snapshot plus this turn's delta, so a new session or a restart starts from the
|
|
7
|
+
same body instead of from zero.
|
|
8
|
+
|
|
9
|
+
Layout:
|
|
10
|
+
{
|
|
11
|
+
"schema": 1,
|
|
12
|
+
"ts": <epoch seconds of the last write>,
|
|
13
|
+
"v": 0.04, "a": 0.52, "stress": 0.2, "energy": 0.69,
|
|
14
|
+
"social": 0.46, "social_pain": 0.0,
|
|
15
|
+
"trust": 0.5, "closeness": 0.5, "conflict": 0.0,
|
|
16
|
+
"intero": {tension, warmth, heaviness, clarity},
|
|
17
|
+
"history": [{"ts", "v", "tension", "warmth", "heaviness", "clarity"}],
|
|
18
|
+
"migrated_sessions": ["<session id>", ...]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
Rules:
|
|
22
|
+
- turn_end folds only the delta (never overwrites) through update();
|
|
23
|
+
- every write is a locked read-modify-write with an atomic replace, so two
|
|
24
|
+
sessions finishing at the same time never lose a delta;
|
|
25
|
+
- migrate_session() merges a legacy session's core fields once per session id;
|
|
26
|
+
- the module never imports the host, never calls a model, never logs.
|
|
27
|
+
"""
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import json
|
|
31
|
+
import os
|
|
32
|
+
import time
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
|
|
35
|
+
SCHEMA = 1
|
|
36
|
+
FIELDS = ("v", "a", "stress", "energy", "social", "social_pain",
|
|
37
|
+
"trust", "closeness", "conflict")
|
|
38
|
+
RANGES = {
|
|
39
|
+
"v": (-1.0, 1.0), "a": (0.0, 1.0), "stress": (0.0, 1.0),
|
|
40
|
+
"energy": (0.0, 1.0), "social": (0.0, 1.0), "social_pain": (0.0, 1.0),
|
|
41
|
+
"trust": (0.0, 1.0), "closeness": (0.0, 1.0), "conflict": (0.0, 1.0),
|
|
42
|
+
}
|
|
43
|
+
DEFAULTS = {
|
|
44
|
+
"v": 0.0, "a": 0.5, "stress": 0.2, "energy": 0.6, "social": 0.55,
|
|
45
|
+
"social_pain": 0.0, "trust": 0.5, "closeness": 0.5, "conflict": 0.0,
|
|
46
|
+
}
|
|
47
|
+
HISTORY_LIMIT = 288
|
|
48
|
+
LOCK_TIMEOUT_S = 3.0
|
|
49
|
+
LOCK_STALE_S = 10.0
|
|
50
|
+
CORE_FILE = "core_state.json"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _num(value, default=0.0):
|
|
54
|
+
try:
|
|
55
|
+
number = float(value)
|
|
56
|
+
except Exception:
|
|
57
|
+
return default
|
|
58
|
+
return number if number == number else default
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _clamp(value, low, high):
|
|
62
|
+
return max(low, min(high, _num(value, low)))
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def path_for(store=None, data_dir=None):
|
|
66
|
+
"""<dataDir>/core_state.json; store is <dataDir>/memory when given."""
|
|
67
|
+
if data_dir is not None and str(data_dir).strip():
|
|
68
|
+
return Path(str(data_dir).strip()) / CORE_FILE
|
|
69
|
+
if store is None:
|
|
70
|
+
return None
|
|
71
|
+
text = str(store).strip()
|
|
72
|
+
if not text:
|
|
73
|
+
return None
|
|
74
|
+
base = Path(text)
|
|
75
|
+
# DSH passes <dataDir>/memory; tests/CLI may pass a bare data dir. Only the
|
|
76
|
+
# "memory" leaf moves the file up one level, so two independent stores can
|
|
77
|
+
# never share (and corrupt) one core_state.json.
|
|
78
|
+
return (base.parent if base.name == "memory" else base) / CORE_FILE
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _normalize(raw):
|
|
82
|
+
data = raw if isinstance(raw, dict) else {}
|
|
83
|
+
doc = {"schema": SCHEMA}
|
|
84
|
+
for key in FIELDS:
|
|
85
|
+
low, high = RANGES[key]
|
|
86
|
+
doc[key] = round(_clamp(data.get(key, DEFAULTS[key]), low, high), 4)
|
|
87
|
+
doc["ts"] = round(_num(data.get("ts"), 0.0), 3)
|
|
88
|
+
intero = data.get("intero") if isinstance(data.get("intero"), dict) else None
|
|
89
|
+
if intero is None:
|
|
90
|
+
doc["intero"] = None
|
|
91
|
+
else:
|
|
92
|
+
doc["intero"] = {key: round(_clamp(intero.get(key), 0.0, 1.0), 4)
|
|
93
|
+
for key in ("tension", "warmth", "heaviness",
|
|
94
|
+
"clarity")}
|
|
95
|
+
history = []
|
|
96
|
+
for row in data.get("history") or []:
|
|
97
|
+
if not isinstance(row, dict):
|
|
98
|
+
continue
|
|
99
|
+
point = {"ts": round(_num(row.get("ts"), 0.0), 3)}
|
|
100
|
+
for key in ("v", "tension", "warmth", "heaviness", "clarity"):
|
|
101
|
+
if row.get(key) is not None:
|
|
102
|
+
point[key] = round(_num(row.get(key), 0.0), 4)
|
|
103
|
+
history.append(point)
|
|
104
|
+
doc["history"] = history[-HISTORY_LIMIT:]
|
|
105
|
+
doc["migrated_sessions"] = [str(item) for item in
|
|
106
|
+
(data.get("migrated_sessions") or [])][-200:]
|
|
107
|
+
return doc
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def load(path):
|
|
111
|
+
"""Normalized document; missing or bad file -> defaults with ts 0."""
|
|
112
|
+
try:
|
|
113
|
+
if path is None or not Path(path).exists():
|
|
114
|
+
return _normalize({})
|
|
115
|
+
return _normalize(json.loads(Path(path).read_text(encoding="utf-8")))
|
|
116
|
+
except Exception:
|
|
117
|
+
return _normalize({})
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def exists(path):
|
|
121
|
+
try:
|
|
122
|
+
return path is not None and Path(path).exists()
|
|
123
|
+
except Exception:
|
|
124
|
+
return False
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def core_of(doc):
|
|
128
|
+
"""The nine authoritative numbers from a normalized document."""
|
|
129
|
+
doc = doc if isinstance(doc, dict) else {}
|
|
130
|
+
return {key: round(_num(doc.get(key), DEFAULTS[key]), 4) for key in FIELDS}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _write_atomic(path, doc):
|
|
134
|
+
path = Path(path)
|
|
135
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
136
|
+
tmp = path.with_name(path.name + ".tmp.%d" % os.getpid())
|
|
137
|
+
tmp.write_text(json.dumps(doc, ensure_ascii=False, indent=2),
|
|
138
|
+
encoding="utf-8")
|
|
139
|
+
os.replace(tmp, path)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _acquire_lock(path):
|
|
143
|
+
lock = Path(str(path) + ".lock")
|
|
144
|
+
try:
|
|
145
|
+
lock.parent.mkdir(parents=True, exist_ok=True)
|
|
146
|
+
except Exception:
|
|
147
|
+
pass
|
|
148
|
+
deadline = time.time() + LOCK_TIMEOUT_S
|
|
149
|
+
while True:
|
|
150
|
+
try:
|
|
151
|
+
fd = os.open(str(lock), os.O_CREAT | os.O_EXCL | os.O_WRONLY)
|
|
152
|
+
try:
|
|
153
|
+
os.write(fd, b"1")
|
|
154
|
+
finally:
|
|
155
|
+
os.close(fd)
|
|
156
|
+
return lock
|
|
157
|
+
except FileExistsError:
|
|
158
|
+
try:
|
|
159
|
+
if time.time() - lock.stat().st_mtime > LOCK_STALE_S:
|
|
160
|
+
lock.unlink()
|
|
161
|
+
continue
|
|
162
|
+
except FileNotFoundError:
|
|
163
|
+
continue
|
|
164
|
+
except Exception:
|
|
165
|
+
pass
|
|
166
|
+
if time.time() >= deadline:
|
|
167
|
+
return None # last-write-wins fallback, still atomic replace
|
|
168
|
+
time.sleep(0.005)
|
|
169
|
+
except OSError:
|
|
170
|
+
if time.time() >= deadline:
|
|
171
|
+
return None
|
|
172
|
+
time.sleep(0.005)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _release(lock):
|
|
176
|
+
if lock is None:
|
|
177
|
+
return
|
|
178
|
+
try:
|
|
179
|
+
lock.unlink()
|
|
180
|
+
except Exception:
|
|
181
|
+
pass
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _mutate(path, fn):
|
|
185
|
+
"""Locked read-modify-write; fn(doc) -> (doc, result) or None to skip."""
|
|
186
|
+
if path is None:
|
|
187
|
+
return None
|
|
188
|
+
lock = _acquire_lock(path)
|
|
189
|
+
try:
|
|
190
|
+
doc = load(path)
|
|
191
|
+
changed = fn(doc)
|
|
192
|
+
if changed is None:
|
|
193
|
+
return None
|
|
194
|
+
doc, result = changed
|
|
195
|
+
doc["schema"] = SCHEMA
|
|
196
|
+
_write_atomic(path, doc)
|
|
197
|
+
return result
|
|
198
|
+
except Exception:
|
|
199
|
+
return None
|
|
200
|
+
finally:
|
|
201
|
+
_release(lock)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def apply(path, deltas=None, absolute=None, now=None, session_id="",
|
|
205
|
+
intero_fn=None, history=True):
|
|
206
|
+
"""Fold one turn into the global state; returns the new document."""
|
|
207
|
+
stamp = round(_num(now, time.time()), 3)
|
|
208
|
+
adds = deltas if isinstance(deltas, dict) else {}
|
|
209
|
+
fixed = absolute if isinstance(absolute, dict) else {}
|
|
210
|
+
|
|
211
|
+
def _fn(doc):
|
|
212
|
+
prev_ts = _num(doc.get("ts"), 0.0)
|
|
213
|
+
dt_hours = 0.0
|
|
214
|
+
if prev_ts > 0 and stamp > prev_ts:
|
|
215
|
+
dt_hours = max(0.0, min(24.0, (stamp - prev_ts) / 3600.0))
|
|
216
|
+
prev_intero = doc.get("intero")
|
|
217
|
+
for key in FIELDS:
|
|
218
|
+
if key in fixed:
|
|
219
|
+
low, high = RANGES[key]
|
|
220
|
+
doc[key] = round(_clamp(fixed[key], low, high), 4)
|
|
221
|
+
elif key in adds:
|
|
222
|
+
low, high = RANGES[key]
|
|
223
|
+
doc[key] = round(_clamp(_num(doc.get(key), DEFAULTS[key])
|
|
224
|
+
+ _num(adds.get(key), 0.0),
|
|
225
|
+
low, high), 4)
|
|
226
|
+
if callable(intero_fn):
|
|
227
|
+
intero = intero_fn(doc, prev_intero, dt_hours, stamp)
|
|
228
|
+
doc["intero"] = (None if intero is None else
|
|
229
|
+
{key: round(_clamp(intero.get(key), 0.0, 1.0), 4)
|
|
230
|
+
for key in ("tension", "warmth", "heaviness",
|
|
231
|
+
"clarity")})
|
|
232
|
+
doc["ts"] = stamp
|
|
233
|
+
if history:
|
|
234
|
+
point = {"ts": stamp, "v": doc["v"]}
|
|
235
|
+
if isinstance(doc.get("intero"), dict):
|
|
236
|
+
point.update(doc["intero"])
|
|
237
|
+
rows = [row for row in doc.get("history") or []
|
|
238
|
+
if isinstance(row, dict)]
|
|
239
|
+
rows.append(point)
|
|
240
|
+
doc["history"] = rows[-HISTORY_LIMIT:]
|
|
241
|
+
if session_id:
|
|
242
|
+
sessions = doc.get("migrated_sessions") or []
|
|
243
|
+
if str(session_id) not in sessions:
|
|
244
|
+
sessions = (sessions + [str(session_id)])[-200:]
|
|
245
|
+
doc["migrated_sessions"] = sessions
|
|
246
|
+
return doc, doc
|
|
247
|
+
|
|
248
|
+
return _mutate(path, _fn)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def migrate_session(path, core=None, session_id="", now=None):
|
|
252
|
+
"""Merge one legacy session's core values once; returns True when merged."""
|
|
253
|
+
values = core if isinstance(core, dict) else {}
|
|
254
|
+
if not values or path is None:
|
|
255
|
+
return False
|
|
256
|
+
stamp = round(_num(now, time.time()), 3)
|
|
257
|
+
sid = str(session_id or "")
|
|
258
|
+
|
|
259
|
+
def _fn(doc):
|
|
260
|
+
sessions = doc.get("migrated_sessions") or []
|
|
261
|
+
if sid and sid in sessions:
|
|
262
|
+
return None
|
|
263
|
+
empty = _num(doc.get("ts"), 0.0) <= 0.0
|
|
264
|
+
if empty:
|
|
265
|
+
for key in FIELDS:
|
|
266
|
+
if key in values:
|
|
267
|
+
low, high = RANGES[key]
|
|
268
|
+
doc[key] = round(_clamp(values.get(key), low, high), 4)
|
|
269
|
+
if isinstance(values.get("intero"), dict):
|
|
270
|
+
doc["intero"] = {key: round(_clamp(values["intero"].get(key),
|
|
271
|
+
0.0, 1.0), 4)
|
|
272
|
+
for key in ("tension", "warmth", "heaviness",
|
|
273
|
+
"clarity")}
|
|
274
|
+
doc["ts"] = round(_num(values.get("ts"), stamp), 3) or stamp
|
|
275
|
+
if sid:
|
|
276
|
+
doc["migrated_sessions"] = (sessions + [sid])[-200:]
|
|
277
|
+
return doc, True
|
|
278
|
+
|
|
279
|
+
return bool(_mutate(path, _fn))
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def snapshot(path):
|
|
283
|
+
"""Read-only view for the /state action."""
|
|
284
|
+
doc = load(path)
|
|
285
|
+
return {
|
|
286
|
+
"ok": True,
|
|
287
|
+
"exists": exists(path),
|
|
288
|
+
"ts": doc.get("ts", 0.0),
|
|
289
|
+
"core": core_of(doc),
|
|
290
|
+
"intero": doc.get("intero"),
|
|
291
|
+
"history": list(doc.get("history") or []),
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def points(path, limit=24):
|
|
296
|
+
"""Newest history points (oldest first), capped at limit."""
|
|
297
|
+
try:
|
|
298
|
+
rows = load(path).get("history") or []
|
|
299
|
+
except Exception:
|
|
300
|
+
return []
|
|
301
|
+
count = max(1, int(limit))
|
|
302
|
+
return rows[-count:]
|