dsh-biocore 0.3.3 → 0.5.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.
Files changed (59) hide show
  1. package/lib/index.js +30 -1
  2. package/lib/modules/standby.js +61 -0
  3. package/lib/modules/state.js +22 -3
  4. package/package.json +1 -1
  5. package/python/biocore_memory/core/config.py +7 -3
  6. package/python/biocore_memory/engine.py +91 -6
  7. package/python/biocore_memory/recall/embedding.py +0 -51
  8. package/python/biocore_memory/recall/feedback.py +34 -7
  9. package/python/biocore_memory/recall/fusion.py +5 -1
  10. package/python/biocore_memory/scoring.py +203 -0
  11. package/python/biocore_memory/space.py +355 -0
  12. package/python/biocore_memory/store/dedupe.py +1 -0
  13. package/python/biocore_memory/store/tiering.py +14 -3
  14. package/python/biocore_model/affective_labor.py +14 -1
  15. package/python/biocore_model/appraisal.py +172 -3
  16. package/python/biocore_model/bio_tick.py +43 -13
  17. package/python/biocore_model/emotion_geometry.py +9 -0
  18. package/python/biocore_model/envy.py +21 -0
  19. package/python/biocore_model/feedback_memory.py +53 -0
  20. package/python/biocore_model/needs_events.py +32 -11
  21. package/python/biocore_model/regulation_strategies.py +5 -0
  22. package/python/biocore_model/repair_after_boundary.py +1 -0
  23. package/python/biocore_model/self_esteem.py +14 -2
  24. package/python/biocore_model/self_intent.py +6 -4
  25. package/python/biocore_model/tests/test_autonomy_self.py +153 -153
  26. package/python/biocore_model/tests/test_cache_series.py +137 -0
  27. package/python/biocore_model/tests/test_core_state.py +240 -240
  28. package/python/biocore_model/tests/test_extensions.py +167 -167
  29. package/python/biocore_model/tests/test_field_arbiter.py +444 -0
  30. package/python/biocore_model/tests/test_governance.py +181 -181
  31. package/python/biocore_model/tests/test_lifecycle.py +153 -0
  32. package/python/biocore_model/tests/test_memory_inject.py +211 -211
  33. package/python/biocore_model/tests/test_retrieval_plus.py +143 -0
  34. package/python/biocore_model/tests/test_signals.py +106 -106
  35. package/python/biocore_model/tests/test_standby.py +199 -0
  36. package/python/biocore_runtime/__init__.py +3 -0
  37. package/python/biocore_runtime/arbiter.py +307 -0
  38. package/python/biocore_runtime/batch_b.py +724 -0
  39. package/python/biocore_runtime/batch_c.py +186 -0
  40. package/python/biocore_runtime/batch_d.py +659 -0
  41. package/python/biocore_runtime/batch_e.py +328 -0
  42. package/python/biocore_runtime/batch_registry.py +446 -0
  43. package/python/biocore_runtime/cache_stats.py +161 -0
  44. package/python/biocore_runtime/core_state.py +106 -3
  45. package/python/biocore_runtime/dimensions.py +419 -0
  46. package/python/biocore_runtime/expression.py +261 -0
  47. package/python/biocore_runtime/field.py +440 -0
  48. package/python/biocore_runtime/lifecycle.py +233 -0
  49. package/python/biocore_runtime/perception.py +1045 -0
  50. package/python/biocore_runtime/pipeline.py +4458 -2974
  51. package/python/biocore_runtime/priority.py +210 -0
  52. package/python/biocore_runtime/retrieval_plus.py +352 -0
  53. package/python/biocore_runtime/standby.py +581 -472
  54. package/python/biocore_runtime/timescales.py +263 -0
  55. package/python/data/signals.json +3266 -206
  56. package/ui/panel-layout.js +32 -29
  57. package/ui/panel.css +563 -974
  58. package/ui/panel.html +427 -401
  59. package/ui/panel.js +543 -450
@@ -1,240 +1,240 @@
1
- # -*- coding: utf-8 -*-
2
- """task-221: global core state + intero/series plumbing (DSH pipeline)."""
3
- import json
4
- import tempfile
5
- import threading
6
- import time
7
- import unittest
8
- from pathlib import Path
9
-
10
- from biocore_runtime import core_state
11
- from biocore_runtime import pipeline
12
-
13
-
14
- def write_json(path, data):
15
- path = Path(path)
16
- path.parent.mkdir(parents=True, exist_ok=True)
17
- path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
18
-
19
-
20
- class CoreStateCase(unittest.TestCase):
21
- def setUp(self):
22
- self.tmp = tempfile.TemporaryDirectory()
23
- self.root = Path(self.tmp.name)
24
- self.data_dir = self.root / "data"
25
- self.store = self.data_dir / "memory"
26
- self.core_file = self.data_dir / "core_state.json"
27
- self.persona = self.root / "missing_persona.json"
28
-
29
- def tearDown(self):
30
- self.tmp.cleanup()
31
-
32
- def payload(self, session_id="s1", **extra):
33
- data = {"store": str(self.store), "session_id": session_id,
34
- "persona_profile": str(self.persona)}
35
- data.update(extra)
36
- return data
37
-
38
- def turn_start(self, session_id="s1", **extra):
39
- return pipeline.dispatch("turn_start", self.payload(
40
- session_id, user_text="hello there", capture=False, **extra))
41
-
42
- def turn_end(self, session_id="s1", **extra):
43
- return pipeline.dispatch("turn_end", self.payload(
44
- session_id, assistant_text="hi", capture=False, **extra))
45
-
46
- def state(self, session_id="s1"):
47
- return pipeline.dispatch("state", self.payload(session_id))
48
-
49
- def session_state(self, session_id="s1"):
50
- path = self.store / "sessions" / session_id / "state.json"
51
- return json.loads(path.read_text(encoding="utf-8"))
52
-
53
-
54
- class PathTest(CoreStateCase):
55
- def test_core_path_is_data_dir(self):
56
- self.assertEqual(
57
- pipeline._core_state_path({"store": str(self.store)}),
58
- self.core_file)
59
-
60
-
61
- class TurnTest(CoreStateCase):
62
- def test_turn_start_creates_global_state(self):
63
- self.turn_start()
64
- self.assertTrue(self.core_file.exists())
65
- doc = json.loads(self.core_file.read_text(encoding="utf-8"))
66
- self.assertGreater(doc["ts"], 0)
67
- self.assertIsInstance(doc["intero"], dict)
68
- self.assertEqual(set(doc["intero"]),
69
- {"tension", "warmth", "heaviness", "clarity"})
70
- session = self.session_state()
71
- self.assertIn("core_delta", session)
72
- self.assertEqual(session["core_delta"], {})
73
-
74
- def test_turn_end_delta_is_additive(self):
75
- self.turn_start()
76
- base = json.loads(self.core_file.read_text(encoding="utf-8"))["stress"]
77
- self.turn_end(state_delta=[{"field": "stress", "before": 0.0,
78
- "after": 0.2}])
79
- mid = json.loads(self.core_file.read_text(encoding="utf-8"))["stress"]
80
- self.assertAlmostEqual(mid, base + 0.2, places=3)
81
- self.turn_end(state_delta=[{"field": "stress", "before": 0.2,
82
- "after": 0.3}])
83
- end = json.loads(self.core_file.read_text(encoding="utf-8"))["stress"]
84
- self.assertAlmostEqual(end, base + 0.3, places=3)
85
-
86
- def test_turn_end_does_not_overwrite(self):
87
- self.turn_start()
88
- start = json.loads(self.core_file.read_text(encoding="utf-8"))["v"]
89
- self.turn_end(delta={"v": -0.05})
90
- self.turn_end(delta={"v": -0.05})
91
- end = json.loads(self.core_file.read_text(encoding="utf-8"))["v"]
92
- self.assertAlmostEqual(end, start - 0.10, places=4)
93
-
94
- def test_turn_end_returns_core_snapshot(self):
95
- self.turn_start()
96
- result = self.turn_end(delta={"v": 0.01})
97
- self.assertTrue(result.get("ok"))
98
- snapshot = result.get("core_state") or {}
99
- self.assertTrue(snapshot.get("exists"))
100
- self.assertIn("intero", snapshot)
101
-
102
-
103
- class CrossSessionTest(CoreStateCase):
104
- def test_two_sessions_share_one_body(self):
105
- self.turn_start("s1")
106
- self.turn_end("s1", delta={"v": 0.02})
107
- self.turn_start("s2")
108
- a = self.state("s1")["state"]
109
- b = self.state("s2")["state"]
110
- self.assertAlmostEqual(a["core"]["v"], b["core"]["v"], places=6)
111
- self.assertEqual(a["intero"], b["intero"])
112
- self.assertGreater(a["core_state_ts"], 0)
113
-
114
- def test_state_prefers_global_intero(self):
115
- self.turn_start()
116
- doc = json.loads(self.core_file.read_text(encoding="utf-8"))
117
- out = self.state()["state"]
118
- self.assertEqual(out["intero"], doc["intero"])
119
- self.assertEqual(out["core_state_ts"], doc["ts"])
120
-
121
- def test_restart_keeps_values(self):
122
- self.turn_start()
123
- self.turn_end(delta={"v": 0.04})
124
- before = self.state()["state"]
125
- # A fresh reader (new process would do the same) sees the same body.
126
- reloaded = core_state.load(self.core_file)
127
- after = self.state()["state"]
128
- self.assertAlmostEqual(reloaded["v"], after["core"]["v"], places=4)
129
- self.assertEqual(before["intero"], after["intero"])
130
- self.assertEqual(before["core_state_ts"], after["core_state_ts"])
131
-
132
- def test_concurrent_updates_do_not_lose_deltas(self):
133
- core_state.apply(self.core_file, absolute={"v": 0.0}, now=time.time())
134
- errors = []
135
-
136
- def worker():
137
- try:
138
- core_state.apply(self.core_file, deltas={"v": 0.01},
139
- now=time.time())
140
- except Exception as error: # pragma: no cover
141
- errors.append(error)
142
-
143
- threads = [threading.Thread(target=worker) for _ in range(20)]
144
- for thread in threads:
145
- thread.start()
146
- for thread in threads:
147
- thread.join()
148
- self.assertEqual(errors, [])
149
- doc = json.loads(self.core_file.read_text(encoding="utf-8"))
150
- self.assertAlmostEqual(doc["v"], 0.20, places=3)
151
-
152
-
153
- class MigrationTest(CoreStateCase):
154
- def test_legacy_session_core_migrates_once(self):
155
- legacy = {"core": {"v": 0.3, "stress": 0.5,
156
- "intero": {"tension": 0.4, "warmth": 0.5,
157
- "heaviness": 0.3, "clarity": 0.6}}}
158
- write_json(self.store / "sessions" / "s1" / "state.json", legacy)
159
- self.turn_start("s1")
160
- first = json.loads(self.core_file.read_text(encoding="utf-8"))
161
- self.assertAlmostEqual(first["v"], 0.3, places=4)
162
- self.assertIn("s1", first["migrated_sessions"])
163
- self.turn_start("s1")
164
- second = json.loads(self.core_file.read_text(encoding="utf-8"))
165
- self.assertAlmostEqual(second["v"], 0.3, places=4)
166
- self.assertEqual(second["migrated_sessions"].count("s1"), 1)
167
-
168
- def test_migration_does_not_override_existing_global(self):
169
- core_state.apply(self.core_file, absolute={"v": 0.1}, now=time.time())
170
- write_json(self.store / "sessions" / "s1" / "state.json",
171
- {"core": {"v": 0.9}})
172
- self.turn_start("s1")
173
- doc = json.loads(self.core_file.read_text(encoding="utf-8"))
174
- self.assertAlmostEqual(doc["v"], 0.1, places=4)
175
-
176
-
177
- class FallbackTest(CoreStateCase):
178
- def test_empty_store_falls_back_to_session_state(self):
179
- write_json(self.store / "sessions" / "s1" / "state.json",
180
- {"core": {"v": 0.2, "stress": 0.6, "energy": 0.4}})
181
- out = self.state("s1")["state"]
182
- self.assertAlmostEqual(out["core"]["v"], 0.2, places=4)
183
- self.assertIn("tension", out["intero"])
184
- self.assertGreater(out["intero"]["tension"], 0.0)
185
- self.assertEqual(out["core_state_ts"], 0.0)
186
-
187
- def test_series_empty_is_array(self):
188
- result = pipeline.dispatch("series", self.payload())
189
- self.assertTrue(result["ok"])
190
- self.assertEqual(result["series"]["v"], [])
191
- self.assertEqual(result["points"], [])
192
-
193
- def test_placeholders_are_zero_not_fake(self):
194
- out = self.state("s1")["state"]
195
- self.assertEqual(out["recall_count"], 0)
196
- self.assertEqual(out["proactive_sent_today"], 0)
197
-
198
- def test_recall_count_reads_traces(self):
199
- trace_dir = self.store / "traces"
200
- trace_dir.mkdir(parents=True, exist_ok=True)
201
- rows = [
202
- {"ts": 1.0, "memory": {"recalled_ids": ["a", "b"]}},
203
- {"ts": 2.0, "memory": {"recalled_ids": ["c"]}},
204
- ]
205
- (trace_dir / "s1.jsonl").write_text(
206
- "\n".join(json.dumps(row) for row in rows) + "\n",
207
- encoding="utf-8")
208
- out = self.state("s1")["state"]
209
- self.assertEqual(out["recall_count"], 3)
210
-
211
- def test_proactive_sent_today_counts(self):
212
- now_ms = int(time.time() * 1000)
213
- write_json(self.store / "proactive.json",
214
- {"lastSentAt": now_ms, "sent": [now_ms]})
215
- out = self.state("s1")["state"]
216
- self.assertEqual(out["proactive_sent_today"], 1)
217
-
218
-
219
- class SeriesTest(CoreStateCase):
220
- def test_series_points_after_turns(self):
221
- self.turn_start()
222
- self.turn_end(delta={"v": 0.02})
223
- self.turn_end(delta={"v": 0.03})
224
- result = pipeline.dispatch("series", self.payload())
225
- points = result["points"]
226
- self.assertGreaterEqual(len(points), 1)
227
- self.assertLessEqual(len(points), 24)
228
- self.assertIn("v", points[-1])
229
- self.assertEqual(result["series"]["v"],
230
- [p.get("v") for p in points if p.get("v") is not None])
231
-
232
- def test_series_merges_growth_history(self):
233
- write_json(self.store / "memory.json", {"entries": []})
234
- (self.store / "growth_history.jsonl").write_text(
235
- json.dumps({"ts": time.time() - 3600, "trust": 0.6,
236
- "closeness": 0.55}) + "\n", encoding="utf-8")
237
- result = pipeline.dispatch("series", self.payload())
238
- self.assertTrue(result["ok"])
239
- self.assertIn("trust", result["series"])
240
- self.assertGreaterEqual(len(result["series"]["trust"]), 1)
1
+ # -*- coding: utf-8 -*-
2
+ """task-221: global core state + intero/series plumbing (DSH pipeline)."""
3
+ import json
4
+ import tempfile
5
+ import threading
6
+ import time
7
+ import unittest
8
+ from pathlib import Path
9
+
10
+ from biocore_runtime import core_state
11
+ from biocore_runtime import pipeline
12
+
13
+
14
+ def write_json(path, data):
15
+ path = Path(path)
16
+ path.parent.mkdir(parents=True, exist_ok=True)
17
+ path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
18
+
19
+
20
+ class CoreStateCase(unittest.TestCase):
21
+ def setUp(self):
22
+ self.tmp = tempfile.TemporaryDirectory()
23
+ self.root = Path(self.tmp.name)
24
+ self.data_dir = self.root / "data"
25
+ self.store = self.data_dir / "memory"
26
+ self.core_file = self.data_dir / "core_state.json"
27
+ self.persona = self.root / "missing_persona.json"
28
+
29
+ def tearDown(self):
30
+ self.tmp.cleanup()
31
+
32
+ def payload(self, session_id="s1", **extra):
33
+ data = {"store": str(self.store), "session_id": session_id,
34
+ "persona_profile": str(self.persona)}
35
+ data.update(extra)
36
+ return data
37
+
38
+ def turn_start(self, session_id="s1", **extra):
39
+ return pipeline.dispatch("turn_start", self.payload(
40
+ session_id, user_text="hello there", capture=False, **extra))
41
+
42
+ def turn_end(self, session_id="s1", **extra):
43
+ return pipeline.dispatch("turn_end", self.payload(
44
+ session_id, assistant_text="hi", capture=False, **extra))
45
+
46
+ def state(self, session_id="s1"):
47
+ return pipeline.dispatch("state", self.payload(session_id))
48
+
49
+ def session_state(self, session_id="s1"):
50
+ path = self.store / "sessions" / session_id / "state.json"
51
+ return json.loads(path.read_text(encoding="utf-8"))
52
+
53
+
54
+ class PathTest(CoreStateCase):
55
+ def test_core_path_is_data_dir(self):
56
+ self.assertEqual(
57
+ pipeline._core_state_path({"store": str(self.store)}),
58
+ self.core_file)
59
+
60
+
61
+ class TurnTest(CoreStateCase):
62
+ def test_turn_start_creates_global_state(self):
63
+ self.turn_start()
64
+ self.assertTrue(self.core_file.exists())
65
+ doc = json.loads(self.core_file.read_text(encoding="utf-8"))
66
+ self.assertGreater(doc["ts"], 0)
67
+ self.assertIsInstance(doc["intero"], dict)
68
+ self.assertEqual(set(doc["intero"]),
69
+ {"tension", "warmth", "heaviness", "clarity"})
70
+ session = self.session_state()
71
+ self.assertIn("core_delta", session)
72
+ self.assertEqual(session["core_delta"], {})
73
+
74
+ def test_turn_end_delta_is_additive(self):
75
+ self.turn_start()
76
+ base = json.loads(self.core_file.read_text(encoding="utf-8"))["stress"]
77
+ self.turn_end(state_delta=[{"field": "stress", "before": 0.0,
78
+ "after": 0.2}])
79
+ mid = json.loads(self.core_file.read_text(encoding="utf-8"))["stress"]
80
+ self.assertAlmostEqual(mid, base + 0.2, places=3)
81
+ self.turn_end(state_delta=[{"field": "stress", "before": 0.2,
82
+ "after": 0.3}])
83
+ end = json.loads(self.core_file.read_text(encoding="utf-8"))["stress"]
84
+ self.assertAlmostEqual(end, base + 0.3, places=3)
85
+
86
+ def test_turn_end_does_not_overwrite(self):
87
+ self.turn_start()
88
+ start = json.loads(self.core_file.read_text(encoding="utf-8"))["v"]
89
+ self.turn_end(delta={"v": -0.05})
90
+ self.turn_end(delta={"v": -0.05})
91
+ end = json.loads(self.core_file.read_text(encoding="utf-8"))["v"]
92
+ self.assertAlmostEqual(end, start - 0.10, places=4)
93
+
94
+ def test_turn_end_returns_core_snapshot(self):
95
+ self.turn_start()
96
+ result = self.turn_end(delta={"v": 0.01})
97
+ self.assertTrue(result.get("ok"))
98
+ snapshot = result.get("core_state") or {}
99
+ self.assertTrue(snapshot.get("exists"))
100
+ self.assertIn("intero", snapshot)
101
+
102
+
103
+ class CrossSessionTest(CoreStateCase):
104
+ def test_two_sessions_share_one_body(self):
105
+ self.turn_start("s1")
106
+ self.turn_end("s1", delta={"v": 0.02})
107
+ self.turn_start("s2")
108
+ a = self.state("s1")["state"]
109
+ b = self.state("s2")["state"]
110
+ self.assertAlmostEqual(a["core"]["v"], b["core"]["v"], places=6)
111
+ self.assertEqual(a["intero"], b["intero"])
112
+ self.assertGreater(a["core_state_ts"], 0)
113
+
114
+ def test_state_prefers_global_intero(self):
115
+ self.turn_start()
116
+ doc = json.loads(self.core_file.read_text(encoding="utf-8"))
117
+ out = self.state()["state"]
118
+ self.assertEqual(out["intero"], doc["intero"])
119
+ self.assertEqual(out["core_state_ts"], doc["ts"])
120
+
121
+ def test_restart_keeps_values(self):
122
+ self.turn_start()
123
+ self.turn_end(delta={"v": 0.04})
124
+ before = self.state()["state"]
125
+ # A fresh reader (new process would do the same) sees the same body.
126
+ reloaded = core_state.load(self.core_file)
127
+ after = self.state()["state"]
128
+ self.assertAlmostEqual(reloaded["v"], after["core"]["v"], places=4)
129
+ self.assertEqual(before["intero"], after["intero"])
130
+ self.assertEqual(before["core_state_ts"], after["core_state_ts"])
131
+
132
+ def test_concurrent_updates_do_not_lose_deltas(self):
133
+ core_state.apply(self.core_file, absolute={"v": 0.0}, now=time.time())
134
+ errors = []
135
+
136
+ def worker():
137
+ try:
138
+ core_state.apply(self.core_file, deltas={"v": 0.01},
139
+ now=time.time())
140
+ except Exception as error: # pragma: no cover
141
+ errors.append(error)
142
+
143
+ threads = [threading.Thread(target=worker) for _ in range(20)]
144
+ for thread in threads:
145
+ thread.start()
146
+ for thread in threads:
147
+ thread.join()
148
+ self.assertEqual(errors, [])
149
+ doc = json.loads(self.core_file.read_text(encoding="utf-8"))
150
+ self.assertAlmostEqual(doc["v"], 0.20, places=3)
151
+
152
+
153
+ class MigrationTest(CoreStateCase):
154
+ def test_legacy_session_core_migrates_once(self):
155
+ legacy = {"core": {"v": 0.3, "stress": 0.5,
156
+ "intero": {"tension": 0.4, "warmth": 0.5,
157
+ "heaviness": 0.3, "clarity": 0.6}}}
158
+ write_json(self.store / "sessions" / "s1" / "state.json", legacy)
159
+ self.turn_start("s1")
160
+ first = json.loads(self.core_file.read_text(encoding="utf-8"))
161
+ self.assertAlmostEqual(first["v"], 0.3, places=4)
162
+ self.assertIn("s1", first["migrated_sessions"])
163
+ self.turn_start("s1")
164
+ second = json.loads(self.core_file.read_text(encoding="utf-8"))
165
+ self.assertAlmostEqual(second["v"], 0.3, places=4)
166
+ self.assertEqual(second["migrated_sessions"].count("s1"), 1)
167
+
168
+ def test_migration_does_not_override_existing_global(self):
169
+ core_state.apply(self.core_file, absolute={"v": 0.1}, now=time.time())
170
+ write_json(self.store / "sessions" / "s1" / "state.json",
171
+ {"core": {"v": 0.9}})
172
+ self.turn_start("s1")
173
+ doc = json.loads(self.core_file.read_text(encoding="utf-8"))
174
+ self.assertAlmostEqual(doc["v"], 0.1, places=4)
175
+
176
+
177
+ class FallbackTest(CoreStateCase):
178
+ def test_empty_store_falls_back_to_session_state(self):
179
+ write_json(self.store / "sessions" / "s1" / "state.json",
180
+ {"core": {"v": 0.2, "stress": 0.6, "energy": 0.4}})
181
+ out = self.state("s1")["state"]
182
+ self.assertAlmostEqual(out["core"]["v"], 0.2, places=4)
183
+ self.assertIn("tension", out["intero"])
184
+ self.assertGreater(out["intero"]["tension"], 0.0)
185
+ self.assertEqual(out["core_state_ts"], 0.0)
186
+
187
+ def test_series_empty_is_array(self):
188
+ result = pipeline.dispatch("series", self.payload())
189
+ self.assertTrue(result["ok"])
190
+ self.assertEqual(result["series"]["v"], [])
191
+ self.assertEqual(result["points"], [])
192
+
193
+ def test_placeholders_are_zero_not_fake(self):
194
+ out = self.state("s1")["state"]
195
+ self.assertEqual(out["recall_count"], 0)
196
+ self.assertEqual(out["proactive_sent_today"], 0)
197
+
198
+ def test_recall_count_reads_traces(self):
199
+ trace_dir = self.store / "traces"
200
+ trace_dir.mkdir(parents=True, exist_ok=True)
201
+ rows = [
202
+ {"ts": 1.0, "memory": {"recalled_ids": ["a", "b"]}},
203
+ {"ts": 2.0, "memory": {"recalled_ids": ["c"]}},
204
+ ]
205
+ (trace_dir / "s1.jsonl").write_text(
206
+ "\n".join(json.dumps(row) for row in rows) + "\n",
207
+ encoding="utf-8")
208
+ out = self.state("s1")["state"]
209
+ self.assertEqual(out["recall_count"], 3)
210
+
211
+ def test_proactive_sent_today_counts(self):
212
+ now_ms = int(time.time() * 1000)
213
+ write_json(self.store / "proactive.json",
214
+ {"lastSentAt": now_ms, "sent": [now_ms]})
215
+ out = self.state("s1")["state"]
216
+ self.assertEqual(out["proactive_sent_today"], 1)
217
+
218
+
219
+ class SeriesTest(CoreStateCase):
220
+ def test_series_points_after_turns(self):
221
+ self.turn_start()
222
+ self.turn_end(delta={"v": 0.02})
223
+ self.turn_end(delta={"v": 0.03})
224
+ result = pipeline.dispatch("series", self.payload())
225
+ points = result["points"]
226
+ self.assertGreaterEqual(len(points), 1)
227
+ self.assertLessEqual(len(points), 24)
228
+ self.assertIn("v", points[-1])
229
+ self.assertEqual(result["series"]["v"],
230
+ [p.get("v") for p in points if p.get("v") is not None])
231
+
232
+ def test_series_merges_growth_history(self):
233
+ write_json(self.store / "memory.json", {"entries": []})
234
+ (self.store / "growth_history.jsonl").write_text(
235
+ json.dumps({"ts": time.time() - 3600, "trust": 0.6,
236
+ "closeness": 0.55}) + "\n", encoding="utf-8")
237
+ result = pipeline.dispatch("series", self.payload())
238
+ self.assertTrue(result["ok"])
239
+ self.assertIn("trust", result["series"])
240
+ self.assertGreaterEqual(len(result["series"]["trust"]), 1)