dsh-biocore 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (250) hide show
  1. package/README.md +2 -2
  2. package/lib/py/bridge.py +15 -7
  3. package/lib/py-bridge.js +4 -3
  4. package/package.json +3 -2
  5. package/python/biocore_memory/__init__.py +21 -0
  6. package/python/biocore_memory/adapters/__init__.py +2 -0
  7. package/python/biocore_memory/adapters/astrbot.py +24 -0
  8. package/python/biocore_memory/adapters/dsh_cli.py +335 -0
  9. package/python/biocore_memory/co_recall.py +324 -0
  10. package/python/biocore_memory/consolidate/__init__.py +11 -0
  11. package/python/biocore_memory/consolidate/gated.py +104 -0
  12. package/python/biocore_memory/consolidate/neighbors.py +68 -0
  13. package/python/biocore_memory/consolidate/reflection.py +45 -0
  14. package/python/biocore_memory/consolidate/tree.py +95 -0
  15. package/python/biocore_memory/consolidate/writeback.py +181 -0
  16. package/python/biocore_memory/core/__init__.py +4 -0
  17. package/python/biocore_memory/core/config.py +127 -0
  18. package/python/biocore_memory/core/permissions.py +69 -0
  19. package/python/biocore_memory/core/types.py +116 -0
  20. package/python/biocore_memory/core/util.py +26 -0
  21. package/python/biocore_memory/embedding_local.py +447 -0
  22. package/python/biocore_memory/embedding_mode.py +156 -0
  23. package/python/biocore_memory/engine.py +2476 -0
  24. package/python/biocore_memory/engine_helpers.py +266 -0
  25. package/python/biocore_memory/event/__init__.py +3 -0
  26. package/python/biocore_memory/event/_types.py +16 -0
  27. package/python/biocore_memory/event/consistency.py +117 -0
  28. package/python/biocore_memory/event/extract.py +48 -0
  29. package/python/biocore_memory/event/media.py +111 -0
  30. package/python/biocore_memory/event/notes.py +85 -0
  31. package/python/biocore_memory/event/notes_link.py +87 -0
  32. package/python/biocore_memory/event/vector.py +69 -0
  33. package/python/biocore_memory/evict/__init__.py +3 -0
  34. package/python/biocore_memory/evict/adaptive.py +73 -0
  35. package/python/biocore_memory/evict/consequence.py +58 -0
  36. package/python/biocore_memory/evict/heat.py +54 -0
  37. package/python/biocore_memory/evict/inhibition.py +105 -0
  38. package/python/biocore_memory/evict/policy.py +188 -0
  39. package/python/biocore_memory/evict/score.py +23 -0
  40. package/python/biocore_memory/identity/__init__.py +6 -0
  41. package/python/biocore_memory/identity/api.py +99 -0
  42. package/python/biocore_memory/identity/history.py +9 -0
  43. package/python/biocore_memory/identity/ledger.py +191 -0
  44. package/python/biocore_memory/identity/narrative.py +358 -0
  45. package/python/biocore_memory/identity/snapshot.py +13 -0
  46. package/python/biocore_memory/identity/writeback.py +61 -0
  47. package/python/biocore_memory/index/__init__.py +3 -0
  48. package/python/biocore_memory/index/cue_index.py +80 -0
  49. package/python/biocore_memory/index/entities.py +85 -0
  50. package/python/biocore_memory/index/entity_boost.py +104 -0
  51. package/python/biocore_memory/index/graph.py +172 -0
  52. package/python/biocore_memory/recall/__init__.py +5 -0
  53. package/python/biocore_memory/recall/api.py +65 -0
  54. package/python/biocore_memory/recall/bm25.py +118 -0
  55. package/python/biocore_memory/recall/channels.py +98 -0
  56. package/python/biocore_memory/recall/complexity.py +62 -0
  57. package/python/biocore_memory/recall/constraints.py +75 -0
  58. package/python/biocore_memory/recall/embedding.py +745 -0
  59. package/python/biocore_memory/recall/expand.py +26 -0
  60. package/python/biocore_memory/recall/filters.py +40 -0
  61. package/python/biocore_memory/recall/fusion.py +18 -0
  62. package/python/biocore_memory/recall/mmr.py +70 -0
  63. package/python/biocore_memory/recall/ppr.py +85 -0
  64. package/python/biocore_memory/recall/retrieval_practice.py +102 -0
  65. package/python/biocore_memory/relay/__init__.py +2 -0
  66. package/python/biocore_memory/relay/card.py +51 -0
  67. package/python/biocore_memory/slots/__init__.py +18 -0
  68. package/python/biocore_memory/slots/admission.py +64 -0
  69. package/python/biocore_memory/slots/boundary.py +128 -0
  70. package/python/biocore_memory/slots/evict_bridge.py +106 -0
  71. package/python/biocore_memory/slots/registry.py +135 -0
  72. package/python/biocore_memory/slots/switch.py +42 -0
  73. package/python/biocore_memory/store/__init__.py +4 -0
  74. package/python/biocore_memory/store/audit.py +74 -0
  75. package/python/biocore_memory/store/compress.py +81 -0
  76. package/python/biocore_memory/store/dedupe.py +178 -0
  77. package/python/biocore_memory/store/json_store.py +154 -0
  78. package/python/biocore_memory/store/media.py +145 -0
  79. package/python/biocore_memory/store/provenance.py +73 -0
  80. package/python/biocore_memory/store/rate_limit.py +93 -0
  81. package/python/biocore_memory/store/security.py +107 -0
  82. package/python/biocore_memory/store/source_consistency.py +123 -0
  83. package/python/biocore_memory/store/tiering.py +61 -0
  84. package/python/biocore_memory/store/tiering_exec.py +50 -0
  85. package/python/biocore_memory/store/tombstone.py +23 -0
  86. package/python/biocore_memory/store/versions.py +69 -0
  87. package/python/biocore_memory/thread/__init__.py +2 -0
  88. package/python/biocore_memory/thread/card.py +58 -0
  89. package/python/biocore_model/__init__.py +37 -0
  90. package/python/biocore_model/affective_labor.py +162 -0
  91. package/python/biocore_model/appraisal.py +257 -0
  92. package/python/biocore_model/appraisal_action.py +78 -0
  93. package/python/biocore_model/archive_queue.py +60 -0
  94. package/python/biocore_model/attachment_behavior.py +130 -0
  95. package/python/biocore_model/attachment_regulation.py +75 -0
  96. package/python/biocore_model/attachment_state.py +41 -0
  97. package/python/biocore_model/attribution.py +73 -0
  98. package/python/biocore_model/attributor.py +162 -0
  99. package/python/biocore_model/autonomic.py +91 -0
  100. package/python/biocore_model/autonomy_affect.py +51 -0
  101. package/python/biocore_model/autonomy_check.py +67 -0
  102. package/python/biocore_model/autonomy_loop.py +122 -0
  103. package/python/biocore_model/autonomy_mode.py +114 -0
  104. package/python/biocore_model/autonomy_model.py +733 -0
  105. package/python/biocore_model/autonomy_params.py +131 -0
  106. package/python/biocore_model/autonomy_reason.py +73 -0
  107. package/python/biocore_model/autonomy_tick.py +227 -0
  108. package/python/biocore_model/bio_appraisal.py +49 -0
  109. package/python/biocore_model/bio_autonomy_state.py +171 -0
  110. package/python/biocore_model/bio_config.py +221 -0
  111. package/python/biocore_model/bio_const.py +128 -0
  112. package/python/biocore_model/bio_daily.py +267 -0
  113. package/python/biocore_model/bio_emotion.py +538 -0
  114. package/python/biocore_model/bio_emotion2.py +214 -0
  115. package/python/biocore_model/bio_goal.py +220 -0
  116. package/python/biocore_model/bio_identity.py +82 -0
  117. package/python/biocore_model/bio_noise.py +119 -0
  118. package/python/biocore_model/bio_physiology2.py +114 -0
  119. package/python/biocore_model/bio_snapshot.py +324 -0
  120. package/python/biocore_model/bio_state.py +272 -0
  121. package/python/biocore_model/bio_tick.py +310 -0
  122. package/python/biocore_model/bio_util.py +82 -0
  123. package/python/biocore_model/blocks.py +264 -0
  124. package/python/biocore_model/boundary.py +251 -0
  125. package/python/biocore_model/boundary_gate.py +188 -0
  126. package/python/biocore_model/commitment_extractor.py +517 -0
  127. package/python/biocore_model/compassion_fatigue.py +84 -0
  128. package/python/biocore_model/connection_bids.py +120 -0
  129. package/python/biocore_model/core_bridge.py +177 -0
  130. package/python/biocore_model/core_client.py +127 -0
  131. package/python/biocore_model/core_commands.py +112 -0
  132. package/python/biocore_model/core_mode.py +104 -0
  133. package/python/biocore_model/core_mode_ui.mjs +104 -0
  134. package/python/biocore_model/criticality.py +81 -0
  135. package/python/biocore_model/defense_mechanisms.py +272 -0
  136. package/python/biocore_model/delta_budget.py +99 -0
  137. package/python/biocore_model/diag_trace.py +274 -0
  138. package/python/biocore_model/dissonance.py +134 -0
  139. package/python/biocore_model/emotion_geometry.py +108 -0
  140. package/python/biocore_model/emotion_probe.py +209 -0
  141. package/python/biocore_model/entity_graph.py +931 -0
  142. package/python/biocore_model/entity_resolve.py +260 -0
  143. package/python/biocore_model/envy.py +193 -0
  144. package/python/biocore_model/exit_or_silence.py +230 -0
  145. package/python/biocore_model/expr_guard.py +24 -0
  146. package/python/biocore_model/expr_intent.py +77 -0
  147. package/python/biocore_model/expr_state.py +204 -0
  148. package/python/biocore_model/expr_template.py +105 -0
  149. package/python/biocore_model/expr_tone.py +81 -0
  150. package/python/biocore_model/extreme_scenarios.py +205 -0
  151. package/python/biocore_model/feedback_flow.py +227 -0
  152. package/python/biocore_model/feedback_guard.py +129 -0
  153. package/python/biocore_model/feedback_llm.py +163 -0
  154. package/python/biocore_model/feedback_memory.py +96 -0
  155. package/python/biocore_model/feedback_user.py +93 -0
  156. package/python/biocore_model/frustration_curve.py +110 -0
  157. package/python/biocore_model/granularity.py +283 -0
  158. package/python/biocore_model/graph_extract.py +248 -0
  159. package/python/biocore_model/graph_recall.py +221 -0
  160. package/python/biocore_model/intent_render.py +91 -0
  161. package/python/biocore_model/intero_core.py +235 -0
  162. package/python/biocore_model/intero_style.py +116 -0
  163. package/python/biocore_model/layered_memory.py +297 -0
  164. package/python/biocore_model/learning_probe.py +120 -0
  165. package/python/biocore_model/memory_dual.py +224 -0
  166. package/python/biocore_model/memory_forgetting.py +125 -0
  167. package/python/biocore_model/memory_store.py +1399 -0
  168. package/python/biocore_model/metacog.py +153 -0
  169. package/python/biocore_model/migrate.py +733 -0
  170. package/python/biocore_model/migrate_store.py +337 -0
  171. package/python/biocore_model/mind_wander.py +111 -0
  172. package/python/biocore_model/mood.py +48 -0
  173. package/python/biocore_model/narrative.py +172 -0
  174. package/python/biocore_model/needs_esteem.py +116 -0
  175. package/python/biocore_model/needs_events.py +137 -0
  176. package/python/biocore_model/needs_sleep.py +72 -0
  177. package/python/biocore_model/noise_bank.py +95 -0
  178. package/python/biocore_model/persona_flow.mjs +25 -0
  179. package/python/biocore_model/pipeline.py +240 -0
  180. package/python/biocore_model/pipeline_api.py +200 -0
  181. package/python/biocore_model/plugin_config.py +83 -0
  182. package/python/biocore_model/plugin_routes.py +1435 -0
  183. package/python/biocore_model/policy_learning.py +132 -0
  184. package/python/biocore_model/possible_selves.py +110 -0
  185. package/python/biocore_model/predictor.py +178 -0
  186. package/python/biocore_model/preset.py +205 -0
  187. package/python/biocore_model/proactive_context.py +153 -0
  188. package/python/biocore_model/prompt_rules.py +90 -0
  189. package/python/biocore_model/prospective.py +183 -0
  190. package/python/biocore_model/provider_fallback.py +97 -0
  191. package/python/biocore_model/reconstruction.py +132 -0
  192. package/python/biocore_model/regulation_loop.py +179 -0
  193. package/python/biocore_model/regulation_strategies.py +294 -0
  194. package/python/biocore_model/relation_stage.py +130 -0
  195. package/python/biocore_model/relationship_stage.py +95 -0
  196. package/python/biocore_model/render.py +901 -0
  197. package/python/biocore_model/render_guard.mjs +34 -0
  198. package/python/biocore_model/repair_after_boundary.py +350 -0
  199. package/python/biocore_model/reply_limit.py +183 -0
  200. package/python/biocore_model/reply_split.py +203 -0
  201. package/python/biocore_model/rollback.py +143 -0
  202. package/python/biocore_model/runtime.py +479 -0
  203. package/python/biocore_model/sanitize.py +200 -0
  204. package/python/biocore_model/scheduler.py +123 -0
  205. package/python/biocore_model/schema.py +120 -0
  206. package/python/biocore_model/secrecy.py +145 -0
  207. package/python/biocore_model/self_esteem.py +50 -0
  208. package/python/biocore_model/self_intent.py +437 -0
  209. package/python/biocore_model/self_interest.py +263 -0
  210. package/python/biocore_model/self_verification.py +178 -0
  211. package/python/biocore_model/services.py +78 -0
  212. package/python/biocore_model/situation.py +254 -0
  213. package/python/biocore_model/sleep_circadian.py +116 -0
  214. package/python/biocore_model/social_pain.py +69 -0
  215. package/python/biocore_model/social_repair.py +111 -0
  216. package/python/biocore_model/social_theory.py +131 -0
  217. package/python/biocore_model/source_monitoring.py +292 -0
  218. package/python/biocore_model/stages.py +487 -0
  219. package/python/biocore_model/state_tag.py +231 -0
  220. package/python/biocore_model/switch_groups.mjs +201 -0
  221. package/python/biocore_model/tabs_state.mjs +37 -0
  222. package/python/biocore_model/template_expr.py +252 -0
  223. package/python/biocore_model/tools/caps_check.mjs +23 -0
  224. package/python/biocore_model/tools/init_symbols_check.mjs +11 -0
  225. package/python/biocore_model/tools/missing_ids.mjs +16 -0
  226. package/python/biocore_model/tools/scope_check.mjs +19 -0
  227. package/python/biocore_model/topic_control.py +268 -0
  228. package/python/biocore_model/turn_memory.py +152 -0
  229. package/python/biocore_model/ui_compat.mjs +48 -0
  230. package/python/biocore_model/ui_settings.py +266 -0
  231. package/python/biocore_model/ui_settings.py.bak_authority +108 -0
  232. package/python/biocore_model/value_order.py +186 -0
  233. package/python/biocore_model/view_mode.mjs +8 -0
  234. package/python/biocore_model/voice_bank.py +177 -0
  235. package/python/biocore_model/waiting_sense.py +229 -0
  236. package/python/biocore_model/x_directives.py +142 -0
  237. package/python/biocore_runtime/__init__.py +7 -0
  238. package/python/biocore_runtime/pipeline.py +1915 -0
  239. package/python/dsh_adapter/README.md +96 -0
  240. package/python/dsh_adapter/adapter.py +1077 -0
  241. package/python/dsh_adapter/cli.py +107 -0
  242. package/python/dsh_adapter/data/demo_state.json +164 -0
  243. package/python/dsh_adapter/data/verify_state.json +164 -0
  244. package/python/dsh_adapter/tests/__init__.py +1 -0
  245. package/python/dsh_adapter/tests/test_adapter.py +494 -0
  246. package/ui/panel-layout.js +3 -3
  247. package/ui/panel.css +53 -6
  248. package/ui/panel.html +33 -32
  249. package/ui/panel.js +9 -1
  250. package/lib/py/__pycache__/bridge.cpython-310.pyc +0 -0
@@ -0,0 +1,324 @@
1
+ # -*- coding: utf-8 -*-
2
+ """task-187: co-recall statistics -- first step: count and mark, never merge.
3
+
4
+ Two memories that keep being recalled in the same batch probably point at the
5
+ same thing. This module only keeps the score; it never changes recall, never
6
+ drops an entry, never merges anything and never calls a model:
7
+
8
+ - pure functions: normalize_id / unique_ids / pair_key / pairs turn one recall
9
+ batch into unordered id pairs (a duplicate inside one batch counts once);
10
+ - thin storage layer: table co_recall(pair_a, pair_b, count, first_ts, last_ts)
11
+ in memory/memory.sqlite3, upserted once per batch;
12
+ - candidates(threshold=3, window_days=30): pairs that crossed the threshold
13
+ inside the recent window (read only);
14
+ - mark_candidates(): writes co_recall_marks (a separate table) so the panel can
15
+ show "mergeable candidates", without touching entries;
16
+ - read_marked() / stats() / readout(): read-only entry points for the panel or
17
+ a command. Wiring into main.py is intentionally left for the caller.
18
+
19
+ Hard boundaries: no LLM call, no change to the recall output or its thresholds,
20
+ no import of main.py. IO lives in the storage layer on purpose.
21
+ """
22
+ from __future__ import annotations
23
+
24
+ import sqlite3
25
+ import time
26
+ from contextlib import contextmanager
27
+ from pathlib import Path
28
+
29
+ DEFAULT_THRESHOLD = 3
30
+ DEFAULT_WINDOW_DAYS = 30.0
31
+ SQLITE_FILE = "memory.sqlite3"
32
+ TABLE_RECALL = "co_recall"
33
+ TABLE_MARKS = "co_recall_marks"
34
+ DAY_SECONDS = 86400.0
35
+ MAX_LIMIT = 500
36
+
37
+ _PLUGIN_ROOT = Path(__file__).resolve().parent.parent
38
+ DEFAULT_DB_PATH = _PLUGIN_ROOT / "memory" / SQLITE_FILE
39
+
40
+ SCHEMA_SQL = """
41
+ CREATE TABLE IF NOT EXISTS co_recall(
42
+ pair_a TEXT NOT NULL,
43
+ pair_b TEXT NOT NULL,
44
+ count INTEGER NOT NULL DEFAULT 0,
45
+ first_ts REAL NOT NULL,
46
+ last_ts REAL NOT NULL,
47
+ PRIMARY KEY (pair_a, pair_b)
48
+ );
49
+ CREATE INDEX IF NOT EXISTS idx_co_recall_last ON co_recall(last_ts);
50
+ CREATE INDEX IF NOT EXISTS idx_co_recall_count ON co_recall(count);
51
+ CREATE TABLE IF NOT EXISTS co_recall_marks(
52
+ pair_a TEXT NOT NULL,
53
+ pair_b TEXT NOT NULL,
54
+ marked_ts REAL NOT NULL,
55
+ threshold INTEGER NOT NULL,
56
+ window_days REAL NOT NULL,
57
+ PRIMARY KEY (pair_a, pair_b)
58
+ );
59
+ """
60
+
61
+
62
+ # ------------------------------------------------------------ pure functions
63
+ def _num(value, default=0.0):
64
+ try:
65
+ number = float(value)
66
+ except Exception:
67
+ return float(default)
68
+ if number != number or number in (float("inf"), float("-inf")):
69
+ return float(default)
70
+ return number
71
+
72
+
73
+ def normalize_id(value):
74
+ """One memory id as a clean string; containers and blanks become ''."""
75
+ if value is None or isinstance(value, (dict, list, tuple, set)):
76
+ return ""
77
+ return str(value).strip()
78
+
79
+
80
+ def unique_ids(ids):
81
+ """Preserve order, drop blanks and duplicates (one batch, one occurrence)."""
82
+ seen = []
83
+ if not isinstance(ids, (list, tuple, set)):
84
+ return seen
85
+ for raw in ids:
86
+ ident = normalize_id(raw)
87
+ if ident and ident not in seen:
88
+ seen.append(ident)
89
+ return seen
90
+
91
+
92
+ def pair_key(a, b):
93
+ """Unordered key (a, b) with a < b; None when it is not a real pair."""
94
+ left = normalize_id(a)
95
+ right = normalize_id(b)
96
+ if not left or not right or left == right:
97
+ return None
98
+ return (left, right) if left < right else (right, left)
99
+
100
+
101
+ def pairs(ids):
102
+ """All unordered pairs from one recall batch, sorted and de-duplicated."""
103
+ row = unique_ids(ids)
104
+ found = set()
105
+ for index in range(len(row)):
106
+ for other in range(index + 1, len(row)):
107
+ key = pair_key(row[index], row[other])
108
+ if key is not None:
109
+ found.add(key)
110
+ return sorted(found)
111
+
112
+
113
+ def qualifies(count, last_ts, threshold=DEFAULT_THRESHOLD,
114
+ window_days=DEFAULT_WINDOW_DAYS, now=None):
115
+ """Pure version of the candidates rule (read-only, no storage)."""
116
+ limit = max(1, int(_num(threshold, DEFAULT_THRESHOLD)))
117
+ days = _num(window_days, DEFAULT_WINDOW_DAYS)
118
+ current = _now(now)
119
+ if int(_num(count, 0.0)) < limit:
120
+ return False
121
+ if days <= 0.0:
122
+ return True
123
+ return _num(last_ts, 0.0) >= current - days * DAY_SECONDS
124
+
125
+
126
+ def _now(now=None):
127
+ try:
128
+ return float(now if now is not None else time.time())
129
+ except Exception:
130
+ return time.time()
131
+
132
+
133
+ # ----------------------------------------------------------- storage layer
134
+ def default_db_path():
135
+ """Where a plugin data dir keeps memory/memory.sqlite3."""
136
+ return DEFAULT_DB_PATH
137
+
138
+
139
+ def ensure_schema(conn):
140
+ """Create the two additive tables; safe to call on every open."""
141
+ conn.executescript(SCHEMA_SQL)
142
+ return True
143
+
144
+
145
+ def connect(db_path=None):
146
+ """Open (and initialize) the memory DB. Caller owns the connection."""
147
+ path = DEFAULT_DB_PATH if db_path is None else Path(db_path)
148
+ if str(path) != ":memory:":
149
+ try:
150
+ path.parent.mkdir(parents=True, exist_ok=True)
151
+ except Exception:
152
+ pass
153
+ conn = sqlite3.connect(str(path), timeout=5.0)
154
+ conn.row_factory = sqlite3.Row
155
+ try:
156
+ conn.execute("PRAGMA busy_timeout=5000")
157
+ except sqlite3.Error:
158
+ pass
159
+ ensure_schema(conn)
160
+ return conn
161
+
162
+
163
+ @contextmanager
164
+ def _session(db_path=None, conn=None):
165
+ owned = conn is None
166
+ db = conn if conn is not None else connect(db_path)
167
+ if not owned:
168
+ ensure_schema(db)
169
+ try:
170
+ yield db
171
+ if owned:
172
+ db.commit()
173
+ finally:
174
+ if owned:
175
+ db.close()
176
+
177
+
178
+ def record_recall(ids, ts=None, db_path=None, conn=None):
179
+ """Count one recall batch. Unordered pairs; duplicate ids counted once."""
180
+ batch = pairs(ids)
181
+ stamp = _now(ts)
182
+ row = unique_ids(ids)
183
+ if not batch:
184
+ return {"pairs": 0, "ids": len(row), "ts": stamp}
185
+ with _session(db_path, conn) as db:
186
+ db.executemany(
187
+ "INSERT INTO co_recall(pair_a, pair_b, count, first_ts, last_ts) "
188
+ "VALUES(?, ?, 1, ?, ?) "
189
+ "ON CONFLICT(pair_a, pair_b) DO UPDATE SET "
190
+ "count = co_recall.count + 1, last_ts = excluded.last_ts",
191
+ [(left, right, stamp, stamp) for left, right in batch])
192
+ return {"pairs": len(batch), "ids": len(row), "ts": stamp}
193
+
194
+
195
+ def _threshold(value):
196
+ return max(1, int(_num(value, DEFAULT_THRESHOLD)))
197
+
198
+
199
+ def _window_days(value):
200
+ return max(0.0, _num(value, DEFAULT_WINDOW_DAYS))
201
+
202
+
203
+ def _row_dict(row, current):
204
+ left = str(row["pair_a"])
205
+ right = str(row["pair_b"])
206
+ last_ts = _num(row["last_ts"], 0.0)
207
+ return {
208
+ "pair": (left, right),
209
+ "pair_a": left,
210
+ "pair_b": right,
211
+ "count": int(_num(row["count"], 0.0)),
212
+ "first_ts": round(_num(row["first_ts"], 0.0), 3),
213
+ "last_ts": round(last_ts, 3),
214
+ "age_days": round(max(0.0, (current - last_ts) / DAY_SECONDS), 2),
215
+ "marked": bool(row["marked"]),
216
+ }
217
+
218
+
219
+ def candidates(threshold=DEFAULT_THRESHOLD,
220
+ window_days=DEFAULT_WINDOW_DAYS, now=None, db_path=None,
221
+ conn=None):
222
+ """Pairs with count >= threshold whose last co-recall is inside the window."""
223
+ limit = _threshold(threshold)
224
+ days = _window_days(window_days)
225
+ current = _now(now)
226
+ sql = ("SELECT r.pair_a, r.pair_b, r.count, r.first_ts, r.last_ts, "
227
+ "CASE WHEN m.pair_a IS NULL THEN 0 ELSE 1 END AS marked "
228
+ "FROM co_recall r LEFT JOIN co_recall_marks m "
229
+ "ON m.pair_a = r.pair_a AND m.pair_b = r.pair_b "
230
+ "WHERE r.count >= ?")
231
+ args = [limit]
232
+ if days > 0.0:
233
+ sql += " AND r.last_ts >= ?"
234
+ args.append(current - days * DAY_SECONDS)
235
+ sql += " ORDER BY r.count DESC, r.last_ts DESC, r.pair_a ASC, r.pair_b ASC"
236
+ with _session(db_path, conn) as db:
237
+ rows = db.execute(sql, args).fetchall()
238
+ return [_row_dict(row, current) for row in rows]
239
+
240
+
241
+ def mark_candidates(threshold=DEFAULT_THRESHOLD,
242
+ window_days=DEFAULT_WINDOW_DAYS, now=None, db_path=None,
243
+ conn=None):
244
+ """Mark over-threshold pairs as mergeable; no deletion, no merging."""
245
+ limit = _threshold(threshold)
246
+ days = _window_days(window_days)
247
+ current = _now(now)
248
+ rows = candidates(limit, days, current, db_path, conn)
249
+ if not rows:
250
+ return {"marked": 0, "threshold": limit, "window_days": days,
251
+ "pairs": []}
252
+ with _session(db_path, conn) as db:
253
+ db.executemany(
254
+ "INSERT INTO co_recall_marks(pair_a, pair_b, marked_ts, threshold, "
255
+ "window_days) VALUES(?, ?, ?, ?, ?) "
256
+ "ON CONFLICT(pair_a, pair_b) DO UPDATE SET "
257
+ "marked_ts = excluded.marked_ts, threshold = excluded.threshold, "
258
+ "window_days = excluded.window_days",
259
+ [(row["pair_a"], row["pair_b"], current, limit, days)
260
+ for row in rows])
261
+ return {"marked": len(rows), "threshold": limit, "window_days": days,
262
+ "pairs": rows}
263
+
264
+
265
+ def read_marked(db_path=None, conn=None, limit=None):
266
+ """Read-only: every marked pair, newest mark first."""
267
+ sql = ("SELECT m.pair_a, m.pair_b, m.marked_ts, m.threshold, "
268
+ "m.window_days, COALESCE(r.count, 0) AS count, "
269
+ "COALESCE(r.first_ts, 0.0) AS first_ts, "
270
+ "COALESCE(r.last_ts, 0.0) AS last_ts "
271
+ "FROM co_recall_marks m LEFT JOIN co_recall r "
272
+ "ON r.pair_a = m.pair_a AND r.pair_b = m.pair_b "
273
+ "ORDER BY m.marked_ts DESC, m.pair_a ASC, m.pair_b ASC")
274
+ args = []
275
+ if limit is not None:
276
+ size = int(_num(limit, 0.0))
277
+ if size > 0:
278
+ sql += " LIMIT ?"
279
+ args.append(min(size, MAX_LIMIT))
280
+ with _session(db_path, conn) as db:
281
+ rows = db.execute(sql, args).fetchall()
282
+ out = []
283
+ for row in rows:
284
+ left = str(row["pair_a"])
285
+ right = str(row["pair_b"])
286
+ out.append({
287
+ "pair": (left, right),
288
+ "pair_a": left,
289
+ "pair_b": right,
290
+ "count": int(_num(row["count"], 0.0)),
291
+ "marked_ts": round(_num(row["marked_ts"], 0.0), 3),
292
+ "threshold": int(_num(row["threshold"], DEFAULT_THRESHOLD)),
293
+ "window_days": round(_num(row["window_days"],
294
+ DEFAULT_WINDOW_DAYS), 2),
295
+ })
296
+ return out
297
+
298
+
299
+ def stats(db_path=None, conn=None):
300
+ """Read-only totals: pairs, co-recall events, marked pairs."""
301
+ with _session(db_path, conn) as db:
302
+ row = db.execute("SELECT COUNT(*) AS pairs, "
303
+ "COALESCE(SUM(count), 0) AS recalls "
304
+ "FROM co_recall").fetchone()
305
+ marked = db.execute("SELECT COUNT(*) AS marked "
306
+ "FROM co_recall_marks").fetchone()
307
+ return {
308
+ "pairs": int(_num(row["pairs"], 0.0)),
309
+ "recalls": int(_num(row["recalls"], 0.0)),
310
+ "marked": int(_num(marked["marked"], 0.0)),
311
+ }
312
+
313
+
314
+ def readout(threshold=DEFAULT_THRESHOLD, window_days=DEFAULT_WINDOW_DAYS,
315
+ now=None, db_path=None, conn=None, limit=20):
316
+ """Bounded read-only view for a panel page or a command reply."""
317
+ limit = max(1, min(int(_num(limit, 20.0)), MAX_LIMIT))
318
+ rows = candidates(threshold, window_days, now, db_path, conn)
319
+ return {
320
+ "threshold": _threshold(threshold),
321
+ "window_days": round(_window_days(window_days), 2),
322
+ "stats": stats(db_path, conn),
323
+ "candidates": rows[:limit],
324
+ }
@@ -0,0 +1,11 @@
1
+ # OWNER: engine-core
2
+ # -*- coding: utf-8 -*-
3
+ """Consolidation mechanisms (default OFF, pure functions, no engine wiring)."""
4
+ from .gated import should_consolidate
5
+ from .tree import build_summary
6
+ from .reflection import propose
7
+ from .neighbors import strengthen
8
+ from .writeback import apply_neighbors, apply_reflections, write_tree
9
+
10
+ __all__ = ["should_consolidate", "build_summary", "propose", "strengthen",
11
+ "apply_reflections", "apply_neighbors", "write_tree"]
@@ -0,0 +1,104 @@
1
+ # -*- coding: utf-8 -*-
2
+ """RecMem-style consolidation gate (pure, read-only, default OFF)."""
3
+ from __future__ import annotations
4
+
5
+ import time
6
+
7
+ GATED_ENABLED = False
8
+ WINDOW_SECONDS = 7 * 86400.0
9
+ MIN_TOPIC_REPEATS = 3
10
+ MIN_GAP_SECONDS = 6 * 3600.0
11
+ COSINE_MERGE = 0.80
12
+ MAX_ITEMS = 200
13
+
14
+
15
+ def _ts(entry):
16
+ try:
17
+ return float(entry.get("ts") or entry.get("created_ts") or 0.0)
18
+ except Exception:
19
+ return 0.0
20
+
21
+
22
+ def _tags(entry):
23
+ raw = entry.get("tags")
24
+ if not isinstance(raw, list):
25
+ raw = (entry.get("metadata") or {}).get("tags") if isinstance(entry.get("metadata"), dict) else []
26
+ return {str(x).strip().lower() for x in (raw or []) if str(x).strip()}
27
+
28
+
29
+ def _topic(entry):
30
+ for key in ("topic", "event_key", "event_name", "thread"):
31
+ value = str(entry.get(key) or "").strip().lower()
32
+ if value:
33
+ return value
34
+ return ""
35
+
36
+
37
+ def _vector(entry):
38
+ vec = entry.get("vector")
39
+ if isinstance(vec, (list, tuple)) and vec:
40
+ return list(vec)
41
+ meta = entry.get("metadata")
42
+ if isinstance(meta, dict):
43
+ vec = meta.get("vector")
44
+ if isinstance(vec, (list, tuple)) and vec:
45
+ return list(vec)
46
+ return []
47
+
48
+
49
+ def _cosine(a, b):
50
+ try:
51
+ if not a or not b or len(a) != len(b):
52
+ return 0.0
53
+ dot = sum(float(x) * float(y) for x, y in zip(a, b))
54
+ na = sum(float(x) ** 2 for x in a) ** 0.5
55
+ nb = sum(float(y) ** 2 for y in b) ** 0.5
56
+ return dot / (na * nb) if na and nb else 0.0
57
+ except Exception:
58
+ return 0.0
59
+
60
+
61
+ def _same_topic(a, b):
62
+ ta, tb = _tags(a), _tags(b)
63
+ if ta and tb and (ta & tb):
64
+ return True
65
+ topic_a, topic_b = _topic(a), _topic(b)
66
+ if topic_a and topic_a == topic_b:
67
+ return True
68
+ return _cosine(_vector(a), _vector(b)) >= COSINE_MERGE
69
+
70
+
71
+ def _last_consolidated(entries):
72
+ last = 0.0
73
+ for entry in entries:
74
+ for key in ("last_consolidated_ts", "consolidated_ts", "consolidate_ts"):
75
+ try:
76
+ last = max(last, float(entry.get(key) or 0.0))
77
+ except Exception:
78
+ continue
79
+ return last
80
+
81
+
82
+ def should_consolidate(entries, now=None):
83
+ """True only when >=3 same-topic items in 7d and last pass was >=6h ago."""
84
+ if not GATED_ENABLED:
85
+ return False
86
+ try:
87
+ rows = [e for e in (entries or []) if isinstance(e, dict)][:MAX_ITEMS]
88
+ if len(rows) < MIN_TOPIC_REPEATS:
89
+ return False
90
+ stamp = float(now if now is not None else time.time())
91
+ recent = [e for e in rows if 0.0 <= stamp - _ts(e) <= WINDOW_SECONDS]
92
+ if len(recent) < MIN_TOPIC_REPEATS:
93
+ return False
94
+ last = _last_consolidated(recent)
95
+ if last and stamp - last < MIN_GAP_SECONDS:
96
+ return False
97
+ for i, entry in enumerate(recent):
98
+ same = 1 + sum(1 for j, other in enumerate(recent)
99
+ if j != i and _same_topic(entry, other))
100
+ if same >= MIN_TOPIC_REPEATS:
101
+ return True
102
+ return False
103
+ except Exception:
104
+ return False
@@ -0,0 +1,68 @@
1
+ # -*- coding: utf-8 -*-
2
+ """A-MEM-style neighbor strengthening proposals (read-only)."""
3
+ from __future__ import annotations
4
+
5
+ NEIGHBORS_ENABLED = False
6
+ COSINE_LINK = 0.80
7
+ MAX_LINKS = 5
8
+
9
+
10
+ def _tags(entry):
11
+ raw = entry.get("tags")
12
+ if not isinstance(raw, list):
13
+ raw = (entry.get("metadata") or {}).get("tags") if isinstance(entry.get("metadata"), dict) else []
14
+ return {str(x).strip().lower() for x in (raw or []) if str(x).strip()}
15
+
16
+
17
+ def _topic(entry):
18
+ for key in ("topic", "event_key", "event_name"):
19
+ value = str(entry.get(key) or "").strip().lower()
20
+ if value:
21
+ return value
22
+ return ""
23
+
24
+
25
+ def _cosine(a, b):
26
+ try:
27
+ if not a or not b or len(a) != len(b):
28
+ return 0.0
29
+ dot = sum(float(x) * float(y) for x, y in zip(a, b))
30
+ na = sum(float(x) ** 2 for x in a) ** 0.5
31
+ nb = sum(float(y) ** 2 for y in b) ** 0.5
32
+ return dot / (na * nb) if na and nb else 0.0
33
+ except Exception:
34
+ return 0.0
35
+
36
+
37
+ def strengthen(entry, neighbors):
38
+ """Suggest bounded links for an entry; returns {entry_id, link_ids, weight}."""
39
+ result = {"entry_id": str((entry or {}).get("id") or "") if isinstance(entry, dict) else "",
40
+ "link_ids": [], "weight": 0.0}
41
+ if not NEIGHBORS_ENABLED or not isinstance(entry, dict):
42
+ return result
43
+ try:
44
+ base_tags, base_topic = _tags(entry), _topic(entry)
45
+ base_vec = entry.get("vector") if isinstance(entry.get("vector"), list) else []
46
+ scored = []
47
+ for other in neighbors or []:
48
+ if not isinstance(other, dict):
49
+ continue
50
+ oid = str(other.get("id") or "")
51
+ if not oid or oid == result["entry_id"]:
52
+ continue
53
+ sim = _cosine(base_vec, other.get("vector") or [])
54
+ if base_tags & _tags(other):
55
+ sim = max(sim, 0.80)
56
+ if base_topic and base_topic == _topic(other):
57
+ sim = max(sim, 0.80)
58
+ if sim >= COSINE_LINK:
59
+ scored.append((sim, oid))
60
+ scored.sort(key=lambda row: (-row[0], row[1]))
61
+ links = [oid for _sim, oid in scored[:MAX_LINKS]]
62
+ weight = round(max(0.0, min(1.0, sum(s for s, _ in scored[:MAX_LINKS])
63
+ / max(1, len(links)))), 4) if links else 0.0
64
+ result["link_ids"] = links
65
+ result["weight"] = weight
66
+ return result
67
+ except Exception:
68
+ return result
@@ -0,0 +1,45 @@
1
+ # -*- coding: utf-8 -*-
2
+ """Reflection stage: propose experience candidates from repeated events (no writes)."""
3
+ from __future__ import annotations
4
+
5
+ REFLECTION_ENABLED = False
6
+ MIN_REPEATS = 2
7
+ MAX_PROPOSALS = 5
8
+ MAX_TEXT = 120
9
+
10
+
11
+ def _key(entry):
12
+ for key in ("event_key", "topic", "event_name"):
13
+ value = str(entry.get(key) or "").strip()
14
+ if value:
15
+ return value[:60]
16
+ return " ".join(str(entry.get("text") or "").split())[:60]
17
+
18
+
19
+ def propose(entries):
20
+ """Return [{text, source_ids}] experience candidates; never writes storage."""
21
+ if not REFLECTION_ENABLED:
22
+ return []
23
+ try:
24
+ groups = {}
25
+ for entry in entries or []:
26
+ if not isinstance(entry, dict):
27
+ continue
28
+ key = _key(entry)
29
+ if key:
30
+ groups.setdefault(key, []).append(entry)
31
+ out = []
32
+ for key, items in sorted(groups.items(), key=lambda kv: -len(kv[1])):
33
+ if len(items) < MIN_REPEATS:
34
+ continue
35
+ sample = " ".join(str(items[-1].get("text") or "").split())[:60]
36
+ text = "经验:关于「%s」的重复经历共 %d 次;最近一次:%s" % (
37
+ key[:40], len(items), sample)
38
+ out.append({"text": text[:MAX_TEXT],
39
+ "source_ids": [str(x.get("id") or "") for x in items[:8]
40
+ if str(x.get("id") or "")]})
41
+ if len(out) >= MAX_PROPOSALS:
42
+ break
43
+ return out
44
+ except Exception:
45
+ return []
@@ -0,0 +1,95 @@
1
+ # -*- coding: utf-8 -*-
2
+ """TiMem-style three-level temporal tree summaries (pure, no writes)."""
3
+ from __future__ import annotations
4
+
5
+ import time
6
+
7
+ TREE_ENABLED = False
8
+ THRESHOLD_L1 = 10
9
+ THRESHOLD_L2 = 5
10
+ THRESHOLD_L3 = 3
11
+ MAX_SOURCE_IDS = 8
12
+
13
+
14
+ def _ts(entry):
15
+ try:
16
+ return float(entry.get("ts") or 0.0)
17
+ except Exception:
18
+ return 0.0
19
+
20
+
21
+ def _day(entry):
22
+ try:
23
+ return time.strftime("%Y-%m-%d", time.gmtime(_ts(entry)))
24
+ except Exception:
25
+ return ""
26
+
27
+
28
+ def _key(entry):
29
+ for key in ("event_key", "topic", "event_name", "thread"):
30
+ value = str(entry.get(key) or "").strip()
31
+ if value:
32
+ return value[:60]
33
+ text = " ".join(str(entry.get("text") or "").split())
34
+ return text[:40] or "untitled"
35
+
36
+
37
+ def _raw(rows):
38
+ return [{"id": str(e.get("id") or ""), "text": str(e.get("text") or "")[:120],
39
+ "ts": _ts(e)} for e in rows]
40
+
41
+
42
+ def _group(rows, key_fn):
43
+ groups = {}
44
+ for entry in rows:
45
+ key = str(key_fn(entry) or "")
46
+ if not key:
47
+ continue
48
+ groups.setdefault(key, []).append(entry)
49
+ out = []
50
+ for key, items in groups.items():
51
+ items = sorted(items, key=_ts)
52
+ out.append({"key": key, "count": len(items),
53
+ "source_ids": [str(x.get("id") or "") for x in items[:MAX_SOURCE_IDS]
54
+ if str(x.get("id") or "")],
55
+ "ts_min": _ts(items[0]), "ts_max": _ts(items[-1]),
56
+ "sample": str(items[-1].get("text") or "")[:120]})
57
+ out.sort(key=lambda row: (-row["count"], row["key"]))
58
+ return out
59
+
60
+
61
+ def build_summary(entries, level=1):
62
+ """Build a level summary only when its threshold is exceeded.
63
+
64
+ Failure/disabled returns the original rows unchanged (items + generated=False).
65
+ """
66
+ try:
67
+ rows = [e for e in (entries or []) if isinstance(e, dict)]
68
+ except Exception:
69
+ rows = []
70
+ try:
71
+ lvl = int(level if level is not None else 1)
72
+ except Exception:
73
+ lvl = 1
74
+ base = {"level": lvl, "items": _raw(rows), "generated": False,
75
+ "enabled": bool(TREE_ENABLED)}
76
+ if not TREE_ENABLED:
77
+ return base
78
+ try:
79
+ if lvl <= 1:
80
+ if len(rows) <= THRESHOLD_L1:
81
+ return base
82
+ return {"level": 1, "items": _raw(rows)[:THRESHOLD_L1],
83
+ "generated": True, "enabled": True, "truncated": len(rows)}
84
+ if lvl == 2:
85
+ if len(rows) <= THRESHOLD_L2:
86
+ return base
87
+ return {"level": 2, "items": _group(rows, _key),
88
+ "generated": True, "enabled": True}
89
+ if len(rows) <= THRESHOLD_L3:
90
+ return base
91
+ return {"level": 3, "items": _group(rows, _day),
92
+ "generated": True, "enabled": True}
93
+ except Exception:
94
+ return {"level": lvl, "items": _raw(rows), "generated": False,
95
+ "enabled": bool(TREE_ENABLED), "error": "summary_failed"}