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,495 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""task-222: DSH memory governance (decay / index / consolidate / graph / aggregate).
|
|
3
|
+
|
|
4
|
+
Operating on the DSH runtime store (<dataDir>/memory/sessions/...), never on
|
|
5
|
+
the engine store. Everything here is additive sidecar state: memory entries
|
|
6
|
+
are only read, never deleted or rewritten. "Decay" lowers the weight used for
|
|
7
|
+
recall ordering, it does not erase anything.
|
|
8
|
+
|
|
9
|
+
Sidecar files under <store>/:
|
|
10
|
+
governance.json per-entry retention score/tier/weight + counters
|
|
11
|
+
index.json token -> entry ids (cue index)
|
|
12
|
+
graph.json concept co-occurrence graph (nodes/edges)
|
|
13
|
+
consolidate.json repeated-topic candidates (hash => idempotent)
|
|
14
|
+
aggregate.json cross-session counts and top topics
|
|
15
|
+
|
|
16
|
+
Public:
|
|
17
|
+
run(store, now) -> summary counters (called once per turn_end)
|
|
18
|
+
lookup(store, query) -> entry ids from the index (counts index hits)
|
|
19
|
+
weight_of(store, entry_id) -> retention weight (1.0 when unknown)
|
|
20
|
+
summary(store) -> counters for /state and /stats
|
|
21
|
+
enabled(store) -> governance flag (default ON, file override)
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import hashlib
|
|
26
|
+
import json
|
|
27
|
+
import math
|
|
28
|
+
import os
|
|
29
|
+
import re
|
|
30
|
+
import time
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
|
|
33
|
+
GOVERNANCE_FILE = "governance.json"
|
|
34
|
+
INDEX_FILE = "index.json"
|
|
35
|
+
GRAPH_FILE = "graph.json"
|
|
36
|
+
CONSOLIDATE_FILE = "consolidate.json"
|
|
37
|
+
AGGREGATE_FILE = "aggregate.json"
|
|
38
|
+
SCHEMA = 1
|
|
39
|
+
MAX_ENTRIES = 400
|
|
40
|
+
COLD_SCORE = 0.35
|
|
41
|
+
WARM_SCORE = 0.55
|
|
42
|
+
COLD_WEIGHT = 0.4
|
|
43
|
+
WARM_WEIGHT = 0.7
|
|
44
|
+
TOPIC_MIN = 2
|
|
45
|
+
MAX_TOPICS = 8
|
|
46
|
+
GRAPH_MIN_COUNT = 2
|
|
47
|
+
MAX_GRAPH_NODES = 80
|
|
48
|
+
MAX_GRAPH_EDGES = 160
|
|
49
|
+
RECENT_EPISODES = 40
|
|
50
|
+
HALF_LIFE_DAYS = 30.0
|
|
51
|
+
THROTTLE_SECONDS = 30.0
|
|
52
|
+
|
|
53
|
+
_WS = re.compile(r"\s+")
|
|
54
|
+
_LATIN = re.compile(r"[a-z][a-z0-9_']*")
|
|
55
|
+
_CJK = re.compile(r"[\u4e00-\u9fff]+")
|
|
56
|
+
_STOP = frozenset((
|
|
57
|
+
"我们", "你们", "他们", "这个", "那个", "就是", "不是", "一个", "什么",
|
|
58
|
+
"怎么", "可以", "没有", "因为", "所以", "然后", "还是", "现在", "今天",
|
|
59
|
+
"明天", "知道", "觉得", "已经", "这样", "那样", "东西", "自己",
|
|
60
|
+
))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _num(value, default=0.0):
|
|
64
|
+
try:
|
|
65
|
+
number = float(value)
|
|
66
|
+
except Exception:
|
|
67
|
+
return default
|
|
68
|
+
return number if number == number else default
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _root(store):
|
|
72
|
+
if isinstance(store, dict):
|
|
73
|
+
raw = store.get("root") or store.get("store") or store.get("path")
|
|
74
|
+
else:
|
|
75
|
+
raw = store
|
|
76
|
+
text = str(raw or "").strip()
|
|
77
|
+
return Path(text) if text else None
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _data_dir(store):
|
|
81
|
+
root = _root(store)
|
|
82
|
+
if root is None:
|
|
83
|
+
return None
|
|
84
|
+
return root.parent if root.name == "memory" else root
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _flags(store):
|
|
88
|
+
data_dir = _data_dir(store)
|
|
89
|
+
raw = {}
|
|
90
|
+
if data_dir is not None:
|
|
91
|
+
try:
|
|
92
|
+
raw = json.loads((data_dir / "flags.json").read_text(encoding="utf-8"))
|
|
93
|
+
except Exception:
|
|
94
|
+
raw = {}
|
|
95
|
+
if not isinstance(raw, dict):
|
|
96
|
+
raw = {}
|
|
97
|
+
return {"governance": raw.get("governance", True) is not False,
|
|
98
|
+
"evict_delete": raw.get("evict_delete") is True,
|
|
99
|
+
"relationship_governance":
|
|
100
|
+
raw.get("relationship_governance", True) is not False}
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def enabled(store):
|
|
104
|
+
if str(os.environ.get("BIOCORE_GOVERNANCE") or "").strip().lower() in (
|
|
105
|
+
"0", "false", "off", "no"):
|
|
106
|
+
return False
|
|
107
|
+
return _flags(store)["governance"]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _read(path, fallback):
|
|
111
|
+
try:
|
|
112
|
+
if path is None or not Path(path).exists():
|
|
113
|
+
return fallback
|
|
114
|
+
data = json.loads(Path(path).read_text(encoding="utf-8"))
|
|
115
|
+
return data if isinstance(data, dict) else fallback
|
|
116
|
+
except Exception:
|
|
117
|
+
return fallback
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _write(path, doc):
|
|
121
|
+
path = Path(path)
|
|
122
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
123
|
+
tmp = path.with_name(path.name + ".tmp")
|
|
124
|
+
tmp.write_text(json.dumps(doc, ensure_ascii=False, indent=2),
|
|
125
|
+
encoding="utf-8")
|
|
126
|
+
os.replace(tmp, path)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _tokens(text):
|
|
130
|
+
lowered = _WS.sub(" ", str(text or "").lower())
|
|
131
|
+
out = []
|
|
132
|
+
for run in _CJK.findall(lowered):
|
|
133
|
+
if len(run) < 2:
|
|
134
|
+
continue
|
|
135
|
+
for index in range(len(run) - 1):
|
|
136
|
+
token = run[index:index + 2]
|
|
137
|
+
if token not in _STOP:
|
|
138
|
+
out.append(token)
|
|
139
|
+
out.extend(word for word in _LATIN.findall(lowered) if len(word) >= 3)
|
|
140
|
+
return out
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def extract_concepts(text, limit=8):
|
|
144
|
+
"""Stable concepts for one entry (unique tokens, first-seen order)."""
|
|
145
|
+
seen = []
|
|
146
|
+
for token in _tokens(text):
|
|
147
|
+
if token not in seen:
|
|
148
|
+
seen.append(token)
|
|
149
|
+
return seen[:max(1, int(limit))]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _entries(store):
|
|
153
|
+
"""Visible entries from every session, newest first."""
|
|
154
|
+
root = _root(store)
|
|
155
|
+
base = (root / "sessions") if root is not None else None
|
|
156
|
+
rows = []
|
|
157
|
+
if base is None or not base.exists():
|
|
158
|
+
return rows
|
|
159
|
+
try:
|
|
160
|
+
sessions = [child for child in base.iterdir() if child.is_dir()]
|
|
161
|
+
except Exception:
|
|
162
|
+
return rows
|
|
163
|
+
for directory in sessions:
|
|
164
|
+
doc = _read(directory / "memory.json", {})
|
|
165
|
+
for entry in doc.get("entries") or []:
|
|
166
|
+
if not isinstance(entry, dict):
|
|
167
|
+
continue
|
|
168
|
+
if entry.get("deleted") or entry.get("archived"):
|
|
169
|
+
continue
|
|
170
|
+
item = dict(entry)
|
|
171
|
+
item["_session"] = directory.name
|
|
172
|
+
rows.append(item)
|
|
173
|
+
rows.sort(key=lambda row: _num(row.get("ts"), 0.0), reverse=True)
|
|
174
|
+
return rows[:MAX_ENTRIES]
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _entry_id(entry):
|
|
178
|
+
return str(entry.get("id") or "").strip()
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def retention(entry, now):
|
|
182
|
+
"""0..1 retention: importance + recency + usage, never negative."""
|
|
183
|
+
ts = _num(entry.get("ts"), 0.0)
|
|
184
|
+
age_days = max(0.0, (now - ts) / 86400.0) if ts > 0 else 0.0
|
|
185
|
+
recency = 0.5 ** (age_days / HALF_LIFE_DAYS)
|
|
186
|
+
importance = max(0.0, min(1.0, _num(entry.get("importance"), 0.5)))
|
|
187
|
+
hits = max(0.0, _num(entry.get("hits"), 1.0))
|
|
188
|
+
usage = min(1.0, hits / 5.0)
|
|
189
|
+
value = max(0.0, min(1.0, _num(entry.get("value"), 0.5)))
|
|
190
|
+
score = 0.40 * importance + 0.30 * recency + 0.20 * usage + 0.10 * value
|
|
191
|
+
return round(max(0.0, min(1.0, score)), 4)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _tier(score):
|
|
195
|
+
if score < COLD_SCORE:
|
|
196
|
+
return "cold", COLD_WEIGHT
|
|
197
|
+
if score < WARM_SCORE:
|
|
198
|
+
return "warm", WARM_WEIGHT
|
|
199
|
+
return "hot", 1.0
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def decay(store, now=None, previous=None):
|
|
203
|
+
"""Recompute retention weights; never deletes or rewrites an entry."""
|
|
204
|
+
stamp = _num(now, time.time())
|
|
205
|
+
prev = previous if isinstance(previous, dict) else {}
|
|
206
|
+
prev_entries = prev.get("entries") if isinstance(prev.get("entries"), dict) else {}
|
|
207
|
+
rows = _entries(store)
|
|
208
|
+
table = {}
|
|
209
|
+
cold = warm = hot = demoted = 0
|
|
210
|
+
for entry in rows:
|
|
211
|
+
entry_id = _entry_id(entry)
|
|
212
|
+
if not entry_id:
|
|
213
|
+
continue
|
|
214
|
+
score = retention(entry, stamp)
|
|
215
|
+
tier, weight = _tier(score)
|
|
216
|
+
old_tier = str((prev_entries.get(entry_id) or {}).get("tier") or "")
|
|
217
|
+
if old_tier and old_tier != "cold" and tier == "cold":
|
|
218
|
+
demoted += 1
|
|
219
|
+
table[entry_id] = {"score": score, "tier": tier,
|
|
220
|
+
"weight": round(weight, 3),
|
|
221
|
+
"session": str(entry.get("_session") or ""),
|
|
222
|
+
"ts": round(_num(entry.get("ts"), 0.0), 3)}
|
|
223
|
+
if tier == "cold":
|
|
224
|
+
cold += 1
|
|
225
|
+
elif tier == "warm":
|
|
226
|
+
warm += 1
|
|
227
|
+
else:
|
|
228
|
+
hot += 1
|
|
229
|
+
return {
|
|
230
|
+
"entries": table,
|
|
231
|
+
"stats": {"total": len(table), "cold": cold, "warm": warm,
|
|
232
|
+
"hot": hot, "demoted": demoted,
|
|
233
|
+
"deleted": 0, "updated_at": round(stamp, 3)},
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def build_index(store):
|
|
238
|
+
"""token -> entry ids; bounded and deterministic."""
|
|
239
|
+
index = {}
|
|
240
|
+
for entry in _entries(store):
|
|
241
|
+
entry_id = _entry_id(entry)
|
|
242
|
+
if not entry_id:
|
|
243
|
+
continue
|
|
244
|
+
for token in dict.fromkeys(_tokens(entry.get("text"))):
|
|
245
|
+
index.setdefault(token, [])
|
|
246
|
+
if entry_id not in index[token]:
|
|
247
|
+
index[token].append(entry_id)
|
|
248
|
+
# keep the index bounded: tokens seen in one entry are noise
|
|
249
|
+
kept = {token: ids for token, ids in index.items() if len(ids) >= 1}
|
|
250
|
+
topics = [token for token, ids in sorted(kept.items(),
|
|
251
|
+
key=lambda item: (-len(item[1]), item[0]))
|
|
252
|
+
if len(ids) >= 2][:MAX_TOPICS]
|
|
253
|
+
latest_ts = max((_num(entry.get("ts"), 0.0) for entry in _entries(store)),
|
|
254
|
+
default=0.0)
|
|
255
|
+
return {"schema": SCHEMA, "terms": kept, "topics": topics,
|
|
256
|
+
"latest_ts": round(latest_ts, 3)}
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def lookup(store, query):
|
|
260
|
+
"""Entry ids matching the query tokens (read; caller counts the hit)."""
|
|
261
|
+
doc = _read((_root(store) or Path(".")) / INDEX_FILE, {})
|
|
262
|
+
terms = doc.get("terms") if isinstance(doc.get("terms"), dict) else {}
|
|
263
|
+
hits = []
|
|
264
|
+
for token in dict.fromkeys(_tokens(query)):
|
|
265
|
+
for entry_id in terms.get(token) or []:
|
|
266
|
+
if entry_id not in hits:
|
|
267
|
+
hits.append(entry_id)
|
|
268
|
+
return hits
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def weight_of(store, entry_id):
|
|
272
|
+
doc = _read((_root(store) or Path(".")) / GOVERNANCE_FILE, {})
|
|
273
|
+
record = (doc.get("entries") or {}).get(str(entry_id))
|
|
274
|
+
return _num((record or {}).get("weight"), 1.0)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def consolidate(store, now=None):
|
|
278
|
+
"""Repeated-topic candidates; hash keeps repeated runs idempotent."""
|
|
279
|
+
stamp = _num(now, time.time())
|
|
280
|
+
candidates = []
|
|
281
|
+
try: # task-223: keep one rule set with the growth report
|
|
282
|
+
from . import growth as _growth
|
|
283
|
+
for row in _growth.candidates(store, RECENT_EPISODES):
|
|
284
|
+
topic = str(row.get("topic") or "")
|
|
285
|
+
if not topic:
|
|
286
|
+
continue
|
|
287
|
+
candidates.append({"topic": topic,
|
|
288
|
+
"count": int(row.get("count") or 0),
|
|
289
|
+
"evidence": list(row.get("evidence") or [])[:5]})
|
|
290
|
+
except Exception:
|
|
291
|
+
candidates = []
|
|
292
|
+
if not candidates:
|
|
293
|
+
episodes = _entries(store)[:RECENT_EPISODES]
|
|
294
|
+
counts = {}
|
|
295
|
+
evidence = {}
|
|
296
|
+
for entry in episodes:
|
|
297
|
+
for token in dict.fromkeys(_tokens(entry.get("text"))):
|
|
298
|
+
counts[token] = counts.get(token, 0) + 1
|
|
299
|
+
evidence.setdefault(token, [])
|
|
300
|
+
entry_id = _entry_id(entry)
|
|
301
|
+
if entry_id and entry_id not in evidence[token]:
|
|
302
|
+
evidence[token].append(entry_id)
|
|
303
|
+
for token, count in sorted(counts.items(), key=lambda item: (-item[1], item[0])):
|
|
304
|
+
if count < TOPIC_MIN:
|
|
305
|
+
continue
|
|
306
|
+
candidates.append({"topic": token, "count": int(count),
|
|
307
|
+
"evidence": evidence.get(token, [])[:5]})
|
|
308
|
+
if len(candidates) >= MAX_TOPICS:
|
|
309
|
+
break
|
|
310
|
+
candidates = candidates[:MAX_TOPICS]
|
|
311
|
+
digest = hashlib.sha256(json.dumps(candidates, ensure_ascii=False,
|
|
312
|
+
sort_keys=True).encode("utf-8")).hexdigest()
|
|
313
|
+
previous = _read((_root(store) or Path(".")) / CONSOLIDATE_FILE, {})
|
|
314
|
+
runs = int(_num(previous.get("runs"), 0)) + 1
|
|
315
|
+
changed = digest != str(previous.get("hash") or "")
|
|
316
|
+
return {"schema": SCHEMA, "ts": round(stamp, 3), "hash": digest,
|
|
317
|
+
"runs": runs, "changed": changed, "candidates": candidates,
|
|
318
|
+
"count": len(candidates)}
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def build_graph(store):
|
|
322
|
+
"""Concept co-occurrence graph over the same bounded entry set."""
|
|
323
|
+
counts = {}
|
|
324
|
+
pairs = {}
|
|
325
|
+
for entry in _entries(store):
|
|
326
|
+
tokens = list(dict.fromkeys(_tokens(entry.get("text"))))
|
|
327
|
+
for token in tokens:
|
|
328
|
+
counts[token] = counts.get(token, 0) + 1
|
|
329
|
+
for index, left in enumerate(tokens):
|
|
330
|
+
for right in tokens[index + 1:]:
|
|
331
|
+
key = tuple(sorted((left, right)))
|
|
332
|
+
pairs[key] = pairs.get(key, 0) + 1
|
|
333
|
+
nodes = [{"id": token, "count": count}
|
|
334
|
+
for token, count in sorted(counts.items(),
|
|
335
|
+
key=lambda item: (-item[1], item[0]))
|
|
336
|
+
if count >= GRAPH_MIN_COUNT][:MAX_GRAPH_NODES]
|
|
337
|
+
|
|
338
|
+
def _is_node(token):
|
|
339
|
+
return any(node["id"] == token for node in nodes)
|
|
340
|
+
|
|
341
|
+
edges = [{"a": left, "b": right, "weight": count}
|
|
342
|
+
for (left, right), count in sorted(
|
|
343
|
+
pairs.items(), key=lambda item: (-item[1], item[0]))
|
|
344
|
+
if count >= GRAPH_MIN_COUNT and _is_node(left) and _is_node(right)]
|
|
345
|
+
return {"schema": SCHEMA, "nodes": nodes, "edges": edges[:MAX_GRAPH_EDGES]}
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def aggregate(store):
|
|
349
|
+
"""Cross-session counts (read-only projection, one row per session)."""
|
|
350
|
+
root = _root(store)
|
|
351
|
+
base = (root / "sessions") if root is not None else None
|
|
352
|
+
sessions = {}
|
|
353
|
+
topics = {}
|
|
354
|
+
if base is not None and base.exists():
|
|
355
|
+
try:
|
|
356
|
+
for directory in base.iterdir():
|
|
357
|
+
if not directory.is_dir():
|
|
358
|
+
continue
|
|
359
|
+
entries = [entry for entry in
|
|
360
|
+
(_read(directory / "memory.json", {}).get("entries") or [])
|
|
361
|
+
if isinstance(entry, dict) and not entry.get("deleted")
|
|
362
|
+
and not entry.get("archived")]
|
|
363
|
+
sessions[directory.name] = len(entries)
|
|
364
|
+
for entry in entries:
|
|
365
|
+
for token in dict.fromkeys(_tokens(entry.get("text"))):
|
|
366
|
+
topics[token] = topics.get(token, 0) + 1
|
|
367
|
+
except Exception:
|
|
368
|
+
pass
|
|
369
|
+
top = [{"topic": token, "count": count}
|
|
370
|
+
for token, count in sorted(topics.items(),
|
|
371
|
+
key=lambda item: (-item[1], item[0]))[:5]]
|
|
372
|
+
return {"schema": SCHEMA, "sessions": sessions,
|
|
373
|
+
"session_count": len(sessions),
|
|
374
|
+
"entries": sum(sessions.values()), "top_topics": top}
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
def summary(store):
|
|
378
|
+
gov = _read((_root(store) or Path(".")) / GOVERNANCE_FILE, {})
|
|
379
|
+
index = _read((_root(store) or Path(".")) / INDEX_FILE, {})
|
|
380
|
+
graph = _read((_root(store) or Path(".")) / GRAPH_FILE, {})
|
|
381
|
+
consolidate_doc = _read((_root(store) or Path(".")) / CONSOLIDATE_FILE, {})
|
|
382
|
+
aggregate_doc = _read((_root(store) or Path(".")) / AGGREGATE_FILE, {})
|
|
383
|
+
stats = gov.get("stats") if isinstance(gov.get("stats"), dict) else {}
|
|
384
|
+
return {
|
|
385
|
+
"decay": {"total": int(_num(stats.get("total"), 0)),
|
|
386
|
+
"cold": int(_num(stats.get("cold"), 0)),
|
|
387
|
+
"warm": int(_num(stats.get("warm"), 0)),
|
|
388
|
+
"hot": int(_num(stats.get("hot"), 0)),
|
|
389
|
+
"decayed": (int(_num(stats.get("cold"), 0))
|
|
390
|
+
+ int(_num(stats.get("warm"), 0))),
|
|
391
|
+
"demoted": int(_num(stats.get("demoted"), 0)),
|
|
392
|
+
"deleted": 0},
|
|
393
|
+
"index": {"terms": len(index.get("terms") or {}),
|
|
394
|
+
"topics": len(index.get("topics") or []),
|
|
395
|
+
"latest_ts": _num(index.get("latest_ts"), 0.0),
|
|
396
|
+
"hits": int(_num(gov.get("index_hits"), 0))},
|
|
397
|
+
"concepts": {"entries": sum(1 for row in _entries(store)
|
|
398
|
+
if row.get("concepts")),
|
|
399
|
+
"filtered": int(_num(gov.get("concept_filters"), 0)),
|
|
400
|
+
"kept": int(_num(gov.get("concept_kept"), 0))},
|
|
401
|
+
"consolidate": {"candidates": len(consolidate_doc.get("candidates") or []),
|
|
402
|
+
"runs": int(_num(consolidate_doc.get("runs"), 0))},
|
|
403
|
+
"graph": {"nodes": len(graph.get("nodes") or []),
|
|
404
|
+
"edges": len(graph.get("edges") or [])},
|
|
405
|
+
"aggregate": {"sessions": int(_num(aggregate_doc.get("session_count"), 0)),
|
|
406
|
+
"entries": int(_num(aggregate_doc.get("entries"), 0))},
|
|
407
|
+
"enabled": enabled(store),
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def run(store, now=None, force=False):
|
|
412
|
+
"""One governance pass; writes sidecars, never touches memory entries."""
|
|
413
|
+
if not enabled(store):
|
|
414
|
+
return {"enabled": False, "skipped": True}
|
|
415
|
+
root = _root(store)
|
|
416
|
+
if root is None:
|
|
417
|
+
return {"enabled": False, "skipped": True}
|
|
418
|
+
stamp = _num(now, time.time())
|
|
419
|
+
gov_path = root / GOVERNANCE_FILE
|
|
420
|
+
previous = _read(gov_path, {})
|
|
421
|
+
last = _num((previous.get("stats") or {}).get("updated_at"), 0.0)
|
|
422
|
+
if not force and last > 0 and stamp - last < THROTTLE_SECONDS:
|
|
423
|
+
summary_doc = summary(store)
|
|
424
|
+
summary_doc["skipped"] = True
|
|
425
|
+
summary_doc["reason"] = "throttle"
|
|
426
|
+
return summary_doc
|
|
427
|
+
gov = decay(store, now=stamp, previous=previous)
|
|
428
|
+
gov["index_hits"] = int(_num(previous.get("index_hits"), 0))
|
|
429
|
+
gov["schema"] = SCHEMA
|
|
430
|
+
gov.setdefault("stats", {})["updated_at"] = round(stamp, 3)
|
|
431
|
+
try:
|
|
432
|
+
_write(gov_path, gov)
|
|
433
|
+
except Exception:
|
|
434
|
+
pass
|
|
435
|
+
index = build_index(store)
|
|
436
|
+
try:
|
|
437
|
+
_write(root / INDEX_FILE, index)
|
|
438
|
+
except Exception:
|
|
439
|
+
pass
|
|
440
|
+
graph = build_graph(store)
|
|
441
|
+
try:
|
|
442
|
+
_write(root / GRAPH_FILE, graph)
|
|
443
|
+
except Exception:
|
|
444
|
+
pass
|
|
445
|
+
consolidation = consolidate(store, now=stamp)
|
|
446
|
+
try:
|
|
447
|
+
_write(root / CONSOLIDATE_FILE, consolidation)
|
|
448
|
+
except Exception:
|
|
449
|
+
pass
|
|
450
|
+
agg = aggregate(store)
|
|
451
|
+
try:
|
|
452
|
+
_write(root / AGGREGATE_FILE, agg)
|
|
453
|
+
except Exception:
|
|
454
|
+
pass
|
|
455
|
+
return {"enabled": True, "decay": gov["stats"],
|
|
456
|
+
"index": {"terms": len(index.get("terms") or {}),
|
|
457
|
+
"hits": gov["index_hits"]},
|
|
458
|
+
"consolidate": {"candidates": consolidation["count"],
|
|
459
|
+
"runs": consolidation["runs"],
|
|
460
|
+
"changed": consolidation["changed"]},
|
|
461
|
+
"graph": {"nodes": len(graph.get("nodes") or []),
|
|
462
|
+
"edges": len(graph.get("edges") or [])},
|
|
463
|
+
"aggregate": {"sessions": agg["session_count"],
|
|
464
|
+
"entries": agg["entries"]}}
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def note_concept_filter(store, kept):
|
|
468
|
+
"""Count how many recalls were narrowed by concepts."""
|
|
469
|
+
root = _root(store)
|
|
470
|
+
if root is None:
|
|
471
|
+
return
|
|
472
|
+
path = root / GOVERNANCE_FILE
|
|
473
|
+
doc = _read(path, {})
|
|
474
|
+
doc["concept_filters"] = int(_num(doc.get("concept_filters"), 0)) + 1
|
|
475
|
+
doc["concept_kept"] = int(_num(doc.get("concept_kept"), 0)) + max(0, int(kept))
|
|
476
|
+
doc["schema"] = SCHEMA
|
|
477
|
+
try:
|
|
478
|
+
_write(path, doc)
|
|
479
|
+
except Exception:
|
|
480
|
+
pass
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
def note_index_hit(store):
|
|
484
|
+
"""Increment the index-hit counter once per query recall."""
|
|
485
|
+
root = _root(store)
|
|
486
|
+
if root is None:
|
|
487
|
+
return
|
|
488
|
+
path = root / GOVERNANCE_FILE
|
|
489
|
+
doc = _read(path, {})
|
|
490
|
+
doc["index_hits"] = int(_num(doc.get("index_hits"), 0)) + 1
|
|
491
|
+
doc["schema"] = SCHEMA
|
|
492
|
+
try:
|
|
493
|
+
_write(path, doc)
|
|
494
|
+
except Exception:
|
|
495
|
+
pass
|