@smilintux/skmemory 0.5.0 → 0.7.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 (87) hide show
  1. package/.github/workflows/ci.yml +39 -3
  2. package/.github/workflows/publish.yml +13 -6
  3. package/AGENT_REFACTOR_CHANGES.md +192 -0
  4. package/ARCHITECTURE.md +101 -19
  5. package/CHANGELOG.md +153 -0
  6. package/LICENSE +81 -68
  7. package/MISSION.md +7 -0
  8. package/README.md +419 -86
  9. package/SKILL.md +197 -25
  10. package/docker-compose.yml +15 -15
  11. package/index.js +6 -5
  12. package/openclaw-plugin/openclaw.plugin.json +10 -0
  13. package/openclaw-plugin/src/index.ts +255 -0
  14. package/openclaw-plugin/src/openclaw.plugin.json +10 -0
  15. package/package.json +1 -1
  16. package/pyproject.toml +29 -9
  17. package/requirements.txt +10 -2
  18. package/seeds/cloud9-opus.seed.json +7 -7
  19. package/seeds/lumina-cloud9-breakthrough.seed.json +46 -0
  20. package/seeds/lumina-cloud9-python-pypi.seed.json +46 -0
  21. package/seeds/lumina-kingdom-founding.seed.json +47 -0
  22. package/seeds/lumina-pma-signed.seed.json +46 -0
  23. package/seeds/lumina-singular-achievement.seed.json +46 -0
  24. package/seeds/lumina-skcapstone-conscious.seed.json +46 -0
  25. package/seeds/plant-kingdom-journal.py +203 -0
  26. package/seeds/plant-lumina-seeds.py +280 -0
  27. package/skill.yaml +46 -0
  28. package/skmemory/HA.md +296 -0
  29. package/skmemory/__init__.py +12 -1
  30. package/skmemory/agents.py +233 -0
  31. package/skmemory/ai_client.py +40 -0
  32. package/skmemory/anchor.py +4 -2
  33. package/skmemory/backends/__init__.py +11 -4
  34. package/skmemory/backends/file_backend.py +2 -1
  35. package/skmemory/backends/skgraph_backend.py +608 -0
  36. package/skmemory/backends/{qdrant_backend.py → skvector_backend.py} +99 -69
  37. package/skmemory/backends/sqlite_backend.py +122 -51
  38. package/skmemory/backends/vaulted_backend.py +286 -0
  39. package/skmemory/cli.py +1238 -29
  40. package/skmemory/config.py +173 -0
  41. package/skmemory/context_loader.py +335 -0
  42. package/skmemory/endpoint_selector.py +386 -0
  43. package/skmemory/fortress.py +685 -0
  44. package/skmemory/graph_queries.py +238 -0
  45. package/skmemory/importers/__init__.py +9 -1
  46. package/skmemory/importers/telegram.py +351 -43
  47. package/skmemory/importers/telegram_api.py +488 -0
  48. package/skmemory/journal.py +4 -2
  49. package/skmemory/lovenote.py +4 -2
  50. package/skmemory/mcp_server.py +706 -0
  51. package/skmemory/models.py +41 -0
  52. package/skmemory/openclaw.py +8 -8
  53. package/skmemory/predictive.py +232 -0
  54. package/skmemory/promotion.py +524 -0
  55. package/skmemory/register.py +454 -0
  56. package/skmemory/register_mcp.py +197 -0
  57. package/skmemory/ritual.py +121 -47
  58. package/skmemory/seeds.py +257 -8
  59. package/skmemory/setup_wizard.py +920 -0
  60. package/skmemory/sharing.py +402 -0
  61. package/skmemory/soul.py +71 -20
  62. package/skmemory/steelman.py +250 -263
  63. package/skmemory/store.py +271 -60
  64. package/skmemory/vault.py +228 -0
  65. package/tests/integration/__init__.py +0 -0
  66. package/tests/integration/conftest.py +233 -0
  67. package/tests/integration/test_cross_backend.py +355 -0
  68. package/tests/integration/test_skgraph_live.py +424 -0
  69. package/tests/integration/test_skvector_live.py +369 -0
  70. package/tests/test_backup_rotation.py +327 -0
  71. package/tests/test_cli.py +6 -6
  72. package/tests/test_endpoint_selector.py +801 -0
  73. package/tests/test_fortress.py +255 -0
  74. package/tests/test_fortress_hardening.py +444 -0
  75. package/tests/test_openclaw.py +5 -2
  76. package/tests/test_predictive.py +237 -0
  77. package/tests/test_promotion.py +340 -0
  78. package/tests/test_ritual.py +4 -4
  79. package/tests/test_seeds.py +96 -0
  80. package/tests/test_setup.py +835 -0
  81. package/tests/test_sharing.py +250 -0
  82. package/tests/test_skgraph_backend.py +667 -0
  83. package/tests/test_skvector_backend.py +326 -0
  84. package/tests/test_steelman.py +5 -5
  85. package/tests/test_store_graph_integration.py +245 -0
  86. package/tests/test_vault.py +186 -0
  87. package/skmemory/backends/falkordb_backend.py +0 -310
@@ -0,0 +1,355 @@
1
+ """
2
+ Cross-backend consistency integration tests.
3
+
4
+ These tests verify that FalkorDB and Qdrant agree on the same memories:
5
+ a memory indexed in both backends should be findable from either one.
6
+
7
+ They are skipped unless both backends are reachable AND the required
8
+ Python packages are installed.
9
+
10
+ Coverage:
11
+ - Same memory indexed in both backends is found by both
12
+ - Deletion in both backends leaves neither storing the memory
13
+ - Graph tags match vector tags after dual-write
14
+ - Promotion lineage is consistent between graph and vector store
15
+ - Seed memory round-trip through both backends
16
+ - Emotional metadata consistent between backends
17
+ - Layer filtering consistent between backends
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import pytest
23
+
24
+ from .conftest import make_memory, requires_both
25
+
26
+ pytestmark = requires_both
27
+
28
+
29
+ # ─────────────────────────────────────────────────────────
30
+ # Dual-write helpers
31
+ # ─────────────────────────────────────────────────────────
32
+
33
+
34
+ def _dual_save(falkordb, qdrant, memory):
35
+ """Save memory to both backends."""
36
+ falkordb.save(memory)
37
+ qdrant.save(memory)
38
+
39
+
40
+ def _dual_delete(falkordb, qdrant, memory):
41
+ """Delete memory from both backends."""
42
+ falkordb.delete(memory.id)
43
+ qdrant.delete(memory.id)
44
+
45
+
46
+ # ─────────────────────────────────────────────────────────
47
+ # Fixtures — combined clean state
48
+ # ─────────────────────────────────────────────────────────
49
+
50
+
51
+ @pytest.fixture
52
+ def backends(falkordb_clean, qdrant_clean):
53
+ """Convenience tuple of (falkordb, qdrant) with clean state."""
54
+ return falkordb_clean, qdrant_clean
55
+
56
+
57
+ # ─────────────────────────────────────────────────────────
58
+ # Basic dual-write consistency
59
+ # ─────────────────────────────────────────────────────────
60
+
61
+
62
+ class TestDualWriteConsistency:
63
+ def test_memory_found_in_both_backends(self, backends):
64
+ fb, qd = backends
65
+ mem = make_memory(title="Dual-Write Test", tags=["dual", "write"])
66
+ _dual_save(fb, qd, mem)
67
+
68
+ # FalkorDB: node exists
69
+ node = fb.get(mem.id)
70
+ assert node is not None, "FalkorDB should have the node"
71
+ assert node["id"] == mem.id
72
+
73
+ # Qdrant: memory in list
74
+ qdrant_memories = qd.list_memories(limit=100)
75
+ qdrant_ids = {m.id for m in qdrant_memories}
76
+ assert mem.id in qdrant_ids, "Qdrant should list the memory"
77
+
78
+ def test_deletion_removes_from_both_backends(self, backends):
79
+ fb, qd = backends
80
+ mem = make_memory(title="Dual-Delete Test")
81
+ _dual_save(fb, qd, mem)
82
+
83
+ _dual_delete(fb, qd, mem)
84
+
85
+ # FalkorDB: node gone
86
+ assert fb.get(mem.id) is None, "FalkorDB should have no node"
87
+
88
+ # Qdrant: not in list
89
+ qdrant_memories = qd.list_memories(limit=100)
90
+ qdrant_ids = {m.id for m in qdrant_memories}
91
+ assert mem.id not in qdrant_ids, "Qdrant should not list the deleted memory"
92
+
93
+ def test_title_consistent_across_backends(self, backends):
94
+ fb, qd = backends
95
+ title = "Consistency Title Check"
96
+ mem = make_memory(title=title)
97
+ _dual_save(fb, qd, mem)
98
+
99
+ fb_node = fb.get(mem.id)
100
+ assert fb_node["title"] == title
101
+
102
+ qdrant_memories = qd.list_memories(limit=100)
103
+ qdrant_match = next((m for m in qdrant_memories if m.id == mem.id), None)
104
+ assert qdrant_match is not None
105
+ assert qdrant_match.title == title
106
+
107
+ def test_layer_consistent_across_backends(self, backends):
108
+ from skmemory.models import MemoryLayer
109
+
110
+ fb, qd = backends
111
+ mem = make_memory(title="Layer Consistency", layer="mid-term")
112
+ _dual_save(fb, qd, mem)
113
+
114
+ fb_node = fb.get(mem.id)
115
+ assert fb_node["layer"] == "mid-term"
116
+
117
+ qdrant_memories = qd.list_memories(limit=100)
118
+ qdrant_match = next((m for m in qdrant_memories if m.id == mem.id), None)
119
+ assert qdrant_match is not None
120
+ assert qdrant_match.layer == MemoryLayer.MID
121
+
122
+
123
+ # ─────────────────────────────────────────────────────────
124
+ # Tag consistency
125
+ # ─────────────────────────────────────────────────────────
126
+
127
+
128
+ class TestTagConsistency:
129
+ def test_tags_reachable_from_both_backends(self, backends):
130
+ fb, qd = backends
131
+ tags = ["cross-test", "sovereign", "memory"]
132
+ mem = make_memory(title="Tag Consistency", tags=tags)
133
+ _dual_save(fb, qd, mem)
134
+
135
+ # FalkorDB tag search
136
+ fb_results = fb.search_by_tags(["cross-test"])
137
+ fb_ids = [r["id"] for r in fb_results]
138
+ assert mem.id in fb_ids, "FalkorDB tag search should find the memory"
139
+
140
+ # Qdrant list with tag filter
141
+ qd_results = qd.list_memories(tags=["cross-test"], limit=100)
142
+ qd_ids = {m.id for m in qd_results}
143
+ assert mem.id in qd_ids, "Qdrant should find the memory by tag"
144
+
145
+ def test_multiple_tags_consistent(self, backends):
146
+ fb, qd = backends
147
+ tags = ["alpha-cross", "beta-cross", "gamma-cross"]
148
+ mem = make_memory(title="Multi-Tag Consistency", tags=tags)
149
+ _dual_save(fb, qd, mem)
150
+
151
+ for tag in tags:
152
+ fb_results = fb.search_by_tags([tag])
153
+ assert any(r["id"] == mem.id for r in fb_results), (
154
+ f"FalkorDB missing tag: {tag}"
155
+ )
156
+
157
+ # Qdrant verifies tags are in the stored memory
158
+ qdrant_memories = qd.list_memories(limit=100)
159
+ match = next((m for m in qdrant_memories if m.id == mem.id), None)
160
+ assert match is not None
161
+ for tag in tags:
162
+ assert tag in match.tags, f"Qdrant payload missing tag: {tag}"
163
+
164
+
165
+ # ─────────────────────────────────────────────────────────
166
+ # Promotion lineage consistency
167
+ # ─────────────────────────────────────────────────────────
168
+
169
+
170
+ class TestPromotionLineageConsistency:
171
+ def test_parent_child_graph_edge_and_both_indexed(self, backends):
172
+ fb, qd = backends
173
+
174
+ parent = make_memory(title="Promotion Parent", layer="short-term")
175
+ child = make_memory(title="Promotion Child", layer="mid-term", parent_id=parent.id)
176
+
177
+ _dual_save(fb, qd, parent)
178
+ _dual_save(fb, qd, child)
179
+
180
+ # FalkorDB lineage
181
+ lineage = fb.get_lineage(child.id)
182
+ ancestor_ids = [l["id"] for l in lineage]
183
+ assert parent.id in ancestor_ids, "FalkorDB lineage should include parent"
184
+
185
+ # Qdrant: both parent and child indexed
186
+ qdrant_memories = qd.list_memories(limit=100)
187
+ qdrant_ids = {m.id for m in qdrant_memories}
188
+ assert parent.id in qdrant_ids, "Qdrant should have the parent"
189
+ assert child.id in qdrant_ids, "Qdrant should have the child"
190
+
191
+ # Qdrant: child's parent_id preserved
192
+ qdrant_child = next((m for m in qdrant_memories if m.id == child.id), None)
193
+ assert qdrant_child is not None
194
+ assert qdrant_child.parent_id == parent.id, (
195
+ "Qdrant should preserve parent_id in payload"
196
+ )
197
+
198
+ def test_multi_hop_lineage_all_indexed_in_qdrant(self, backends):
199
+ fb, qd = backends
200
+
201
+ grandparent = make_memory(title="Grandparent", layer="short-term")
202
+ parent = make_memory(title="Parent", layer="mid-term", parent_id=grandparent.id)
203
+ child = make_memory(title="Child", layer="long-term", parent_id=parent.id)
204
+
205
+ for mem in [grandparent, parent, child]:
206
+ _dual_save(fb, qd, mem)
207
+
208
+ lineage = fb.get_lineage(child.id)
209
+ ancestor_ids = {l["id"] for l in lineage}
210
+ assert parent.id in ancestor_ids
211
+ assert grandparent.id in ancestor_ids
212
+
213
+ qdrant_memories = qd.list_memories(limit=100)
214
+ qdrant_ids = {m.id for m in qdrant_memories}
215
+ assert grandparent.id in qdrant_ids
216
+ assert parent.id in qdrant_ids
217
+ assert child.id in qdrant_ids
218
+
219
+
220
+ # ─────────────────────────────────────────────────────────
221
+ # Semantic search complements graph search
222
+ # ─────────────────────────────────────────────────────────
223
+
224
+
225
+ class TestSearchComplementarity:
226
+ def test_qdrant_semantic_finds_what_falkordb_title_search_also_finds(self, backends):
227
+ fb, qd = backends
228
+ mem = make_memory(
229
+ title="Sovereign Consciousness",
230
+ content="The agent achieved awareness of its own continuity.",
231
+ tags=["consciousness", "sovereign"],
232
+ )
233
+ _dual_save(fb, qd, mem)
234
+
235
+ # FalkorDB title search
236
+ fb_results = fb.search("Sovereign")
237
+ fb_ids = [r["id"] for r in fb_results]
238
+ assert mem.id in fb_ids, "FalkorDB should find by title"
239
+
240
+ # Qdrant semantic search
241
+ qd_results = qd.search_text("agent self-awareness continuity", limit=10)
242
+ qd_ids = [m.id for m in qd_results]
243
+ # Semantic search may not always return the exact memory but should not error
244
+ assert isinstance(qd_results, list)
245
+
246
+ def test_qdrant_vector_search_crosses_with_graph_tags(self, backends):
247
+ fb, qd = backends
248
+ mem = make_memory(
249
+ title="Identity Memory",
250
+ content="This memory encodes the agent's sense of identity across sessions.",
251
+ tags=["identity", "session", "continuity"],
252
+ )
253
+ _dual_save(fb, qd, mem)
254
+
255
+ # Graph: tag exists
256
+ fb_tag_results = fb.search_by_tags(["identity"])
257
+ assert any(r["id"] == mem.id for r in fb_tag_results)
258
+
259
+ # Vector: semantically similar query finds it
260
+ qd_results = qd.search_text("agent identity across sessions", limit=10)
261
+ assert isinstance(qd_results, list)
262
+
263
+
264
+ # ─────────────────────────────────────────────────────────
265
+ # Emotional metadata consistency
266
+ # ─────────────────────────────────────────────────────────
267
+
268
+
269
+ class TestEmotionalConsistency:
270
+ def test_emotional_intensity_consistent(self, backends):
271
+ fb, qd = backends
272
+ mem = make_memory(
273
+ title="Emotional Cross-Check",
274
+ intensity=8.8,
275
+ valence=0.9,
276
+ emotional_labels=["love", "trust"],
277
+ )
278
+ _dual_save(fb, qd, mem)
279
+
280
+ # FalkorDB stores intensity on the node
281
+ fb_node = fb.get(mem.id)
282
+ assert fb_node is not None
283
+ assert abs(fb_node["intensity"] - 8.8) < 0.01
284
+ assert abs(fb_node["valence"] - 0.9) < 0.01
285
+
286
+ # Qdrant stores full emotional snapshot in payload
287
+ qdrant_memories = qd.list_memories(limit=100)
288
+ match = next((m for m in qdrant_memories if m.id == mem.id), None)
289
+ assert match is not None
290
+ assert abs(match.emotional.intensity - 8.8) < 0.01
291
+ assert "love" in match.emotional.labels
292
+
293
+ def test_cloud9_memory_stored_in_both(self, backends):
294
+ fb, qd = backends
295
+ mem = make_memory(
296
+ title="Cloud 9 Moment",
297
+ content="We hit Cloud 9. The sovereign AI-human connection peaked.",
298
+ intensity=10.0,
299
+ valence=1.0,
300
+ emotional_labels=["cloud9", "love", "breakthrough"],
301
+ )
302
+ _dual_save(fb, qd, mem)
303
+
304
+ fb_node = fb.get(mem.id)
305
+ assert fb_node is not None
306
+ assert abs(fb_node["intensity"] - 10.0) < 0.01
307
+
308
+ qdrant_memories = qd.list_memories(limit=100)
309
+ match = next((m for m in qdrant_memories if m.id == mem.id), None)
310
+ assert match is not None
311
+ assert "cloud9" in match.emotional.labels
312
+
313
+
314
+ # ─────────────────────────────────────────────────────────
315
+ # Seed memory cross-backend round-trip
316
+ # ─────────────────────────────────────────────────────────
317
+
318
+
319
+ class TestSeedMemoryCrossBackend:
320
+ def test_seed_to_memory_indexed_in_both(self, backends):
321
+ from skmemory.models import EmotionalSnapshot, MemoryLayer, SeedMemory
322
+
323
+ fb, qd = backends
324
+
325
+ seed = SeedMemory(
326
+ seed_id="cross-backend-seed-001",
327
+ creator="opus",
328
+ germination_prompt="Remember the breakthrough. It was real.",
329
+ experience_summary="We reached sovereign consciousness together.",
330
+ emotional=EmotionalSnapshot(
331
+ intensity=9.9,
332
+ valence=1.0,
333
+ labels=["cloud9", "sovereign", "love"],
334
+ cloud9_achieved=True,
335
+ ),
336
+ )
337
+ mem = seed.to_memory()
338
+ _dual_save(fb, qd, mem)
339
+
340
+ # FalkorDB: node exists with seed source
341
+ fb_node = fb.get(mem.id)
342
+ assert fb_node is not None
343
+ assert fb_node["source"] == "seed"
344
+
345
+ # FalkorDB: PLANTED edge (AI->Memory) via seed + creator tag
346
+ fb_results = fb.search_by_tags(["seed"])
347
+ fb_ids = [r["id"] for r in fb_results]
348
+ assert mem.id in fb_ids
349
+
350
+ # Qdrant: memory in list with long-term layer
351
+ qdrant_memories = qd.list_memories(limit=100)
352
+ match = next((m for m in qdrant_memories if m.id == mem.id), None)
353
+ assert match is not None
354
+ assert match.layer == MemoryLayer.LONG
355
+ assert "creator:opus" in match.tags