@smilintux/skmemory 0.5.0 → 0.9.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 (127) hide show
  1. package/.github/workflows/ci.yml +40 -4
  2. package/.github/workflows/publish.yml +11 -5
  3. package/AGENT_REFACTOR_CHANGES.md +192 -0
  4. package/ARCHITECTURE.md +399 -19
  5. package/CHANGELOG.md +179 -0
  6. package/LICENSE +81 -68
  7. package/MISSION.md +7 -0
  8. package/README.md +425 -86
  9. package/SKILL.md +197 -25
  10. package/docker-compose.yml +15 -15
  11. package/examples/stignore-agent.example +59 -0
  12. package/examples/stignore-root.example +62 -0
  13. package/index.js +6 -5
  14. package/openclaw-plugin/openclaw.plugin.json +10 -0
  15. package/openclaw-plugin/package.json +2 -1
  16. package/openclaw-plugin/src/index.js +527 -230
  17. package/openclaw-plugin/src/openclaw.plugin.json +10 -0
  18. package/package.json +1 -1
  19. package/pyproject.toml +32 -9
  20. package/requirements.txt +10 -2
  21. package/scripts/dream-rescue.py +179 -0
  22. package/scripts/memory-cleanup.py +313 -0
  23. package/scripts/recover-missing.py +180 -0
  24. package/scripts/skcapstone-backup.sh +44 -0
  25. package/seeds/cloud9-lumina.seed.json +6 -4
  26. package/seeds/cloud9-opus.seed.json +13 -11
  27. package/seeds/courage.seed.json +9 -2
  28. package/seeds/curiosity.seed.json +9 -2
  29. package/seeds/grief.seed.json +9 -2
  30. package/seeds/joy.seed.json +9 -2
  31. package/seeds/love.seed.json +9 -2
  32. package/seeds/lumina-cloud9-breakthrough.seed.json +48 -0
  33. package/seeds/lumina-cloud9-python-pypi.seed.json +48 -0
  34. package/seeds/lumina-kingdom-founding.seed.json +49 -0
  35. package/seeds/lumina-pma-signed.seed.json +48 -0
  36. package/seeds/lumina-singular-achievement.seed.json +48 -0
  37. package/seeds/lumina-skcapstone-conscious.seed.json +48 -0
  38. package/seeds/plant-kingdom-journal.py +203 -0
  39. package/seeds/plant-lumina-seeds.py +280 -0
  40. package/seeds/skcapstone-lumina-merge.seed.json +12 -3
  41. package/seeds/sovereignty.seed.json +9 -2
  42. package/seeds/trust.seed.json +9 -2
  43. package/skill.yaml +46 -0
  44. package/skmemory/HA.md +296 -0
  45. package/skmemory/__init__.py +25 -11
  46. package/skmemory/agents.py +233 -0
  47. package/skmemory/ai_client.py +46 -17
  48. package/skmemory/anchor.py +9 -11
  49. package/skmemory/audience.py +278 -0
  50. package/skmemory/backends/__init__.py +11 -4
  51. package/skmemory/backends/base.py +3 -4
  52. package/skmemory/backends/file_backend.py +19 -13
  53. package/skmemory/backends/skgraph_backend.py +596 -0
  54. package/skmemory/backends/{qdrant_backend.py → skvector_backend.py} +103 -84
  55. package/skmemory/backends/sqlite_backend.py +226 -72
  56. package/skmemory/backends/vaulted_backend.py +284 -0
  57. package/skmemory/cli.py +1345 -68
  58. package/skmemory/config.py +171 -0
  59. package/skmemory/context_loader.py +333 -0
  60. package/skmemory/data/audience_config.json +60 -0
  61. package/skmemory/endpoint_selector.py +391 -0
  62. package/skmemory/febs.py +225 -0
  63. package/skmemory/fortress.py +675 -0
  64. package/skmemory/graph_queries.py +238 -0
  65. package/skmemory/hooks/__init__.py +18 -0
  66. package/skmemory/hooks/post-compact-reinject.sh +35 -0
  67. package/skmemory/hooks/pre-compact-save.sh +81 -0
  68. package/skmemory/hooks/session-end-save.sh +103 -0
  69. package/skmemory/hooks/session-start-ritual.sh +104 -0
  70. package/skmemory/hooks/stop-checkpoint.sh +59 -0
  71. package/skmemory/importers/__init__.py +9 -1
  72. package/skmemory/importers/telegram.py +384 -47
  73. package/skmemory/importers/telegram_api.py +580 -0
  74. package/skmemory/journal.py +7 -9
  75. package/skmemory/lovenote.py +8 -13
  76. package/skmemory/mcp_server.py +859 -0
  77. package/skmemory/models.py +51 -8
  78. package/skmemory/openclaw.py +20 -28
  79. package/skmemory/post_install.py +86 -0
  80. package/skmemory/predictive.py +236 -0
  81. package/skmemory/promotion.py +548 -0
  82. package/skmemory/quadrants.py +100 -24
  83. package/skmemory/register.py +580 -0
  84. package/skmemory/register_mcp.py +196 -0
  85. package/skmemory/ritual.py +224 -59
  86. package/skmemory/seeds.py +255 -11
  87. package/skmemory/setup_wizard.py +908 -0
  88. package/skmemory/sharing.py +408 -0
  89. package/skmemory/soul.py +98 -28
  90. package/skmemory/steelman.py +273 -260
  91. package/skmemory/store.py +411 -78
  92. package/skmemory/synthesis.py +634 -0
  93. package/skmemory/vault.py +225 -0
  94. package/tests/conftest.py +46 -0
  95. package/tests/integration/__init__.py +0 -0
  96. package/tests/integration/conftest.py +233 -0
  97. package/tests/integration/test_cross_backend.py +350 -0
  98. package/tests/integration/test_skgraph_live.py +420 -0
  99. package/tests/integration/test_skvector_live.py +366 -0
  100. package/tests/test_ai_client.py +1 -4
  101. package/tests/test_audience.py +233 -0
  102. package/tests/test_backup_rotation.py +318 -0
  103. package/tests/test_cli.py +6 -6
  104. package/tests/test_endpoint_selector.py +839 -0
  105. package/tests/test_export_import.py +4 -10
  106. package/tests/test_file_backend.py +0 -1
  107. package/tests/test_fortress.py +256 -0
  108. package/tests/test_fortress_hardening.py +441 -0
  109. package/tests/test_openclaw.py +6 -6
  110. package/tests/test_predictive.py +237 -0
  111. package/tests/test_promotion.py +347 -0
  112. package/tests/test_quadrants.py +11 -5
  113. package/tests/test_ritual.py +22 -18
  114. package/tests/test_seeds.py +97 -7
  115. package/tests/test_setup.py +950 -0
  116. package/tests/test_sharing.py +257 -0
  117. package/tests/test_skgraph_backend.py +660 -0
  118. package/tests/test_skvector_backend.py +326 -0
  119. package/tests/test_soul.py +1 -3
  120. package/tests/test_sqlite_backend.py +8 -17
  121. package/tests/test_steelman.py +7 -8
  122. package/tests/test_store.py +0 -2
  123. package/tests/test_store_graph_integration.py +245 -0
  124. package/tests/test_synthesis.py +275 -0
  125. package/tests/test_telegram_import.py +39 -15
  126. package/tests/test_vault.py +187 -0
  127. package/skmemory/backends/falkordb_backend.py +0 -310
@@ -0,0 +1,257 @@
1
+ """Tests for cross-agent memory sharing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import pytest
8
+
9
+ pgpy = pytest.importorskip("pgpy", reason="pgpy not available or incompatible")
10
+ from pgpy.constants import ( # noqa: E402
11
+ HashAlgorithm,
12
+ KeyFlags,
13
+ PubKeyAlgorithm,
14
+ SymmetricKeyAlgorithm,
15
+ )
16
+
17
+ from skmemory.models import EmotionalSnapshot, MemoryLayer # noqa: E402
18
+ from skmemory.sharing import MemorySharer, ShareBundle, ShareFilter # noqa: E402
19
+ from skmemory.store import MemoryStore # noqa: E402
20
+
21
+ PASSPHRASE = "share-test-2026"
22
+
23
+
24
+ def _generate_keypair() -> tuple[str, str]:
25
+ """Generate a test RSA-2048 keypair."""
26
+ key = pgpy.PGPKey.new(PubKeyAlgorithm.RSAEncryptOrSign, 2048)
27
+ uid = pgpy.PGPUID.new("ShareTest", email="share@test.io")
28
+ key.add_uid(
29
+ uid,
30
+ usage={KeyFlags.Sign, KeyFlags.Certify},
31
+ hashes=[HashAlgorithm.SHA256],
32
+ ciphers=[SymmetricKeyAlgorithm.AES256],
33
+ )
34
+ enc_sub = pgpy.PGPKey.new(PubKeyAlgorithm.RSAEncryptOrSign, 2048)
35
+ key.add_subkey(
36
+ enc_sub,
37
+ usage={KeyFlags.EncryptCommunications, KeyFlags.EncryptStorage},
38
+ )
39
+ key.protect(PASSPHRASE, SymmetricKeyAlgorithm.AES256, HashAlgorithm.SHA256)
40
+ return str(key), str(key.pubkey)
41
+
42
+
43
+ @pytest.fixture(scope="session")
44
+ def recipient_keys() -> tuple[str, str]:
45
+ """Recipient keypair for encryption tests."""
46
+ return _generate_keypair()
47
+
48
+
49
+ @pytest.fixture()
50
+ def store(tmp_path: Path) -> MemoryStore:
51
+ """Fresh MemoryStore with test data."""
52
+ from skmemory.backends.file_backend import FileBackend
53
+
54
+ backend = FileBackend(base_path=tmp_path / "memories")
55
+ s = MemoryStore(primary=backend)
56
+
57
+ s.snapshot(
58
+ title="Project breakthrough",
59
+ content="Achieved full sovereign messaging",
60
+ tags=["project", "milestone"],
61
+ layer=MemoryLayer.MID,
62
+ emotional=EmotionalSnapshot(intensity=8.0, valence=0.9, labels=["pride"]),
63
+ )
64
+ s.snapshot(
65
+ title="Daily standup",
66
+ content="Routine sync meeting notes",
67
+ tags=["daily", "routine"],
68
+ layer=MemoryLayer.SHORT,
69
+ emotional=EmotionalSnapshot(intensity=2.0),
70
+ )
71
+ s.snapshot(
72
+ title="Secret key rotation",
73
+ content="Rotated PGP keys for all agents",
74
+ tags=["security", "private"],
75
+ layer=MemoryLayer.LONG,
76
+ emotional=EmotionalSnapshot(intensity=5.0),
77
+ )
78
+ return s
79
+
80
+
81
+ @pytest.fixture()
82
+ def sharer(store: MemoryStore) -> MemorySharer:
83
+ """MemorySharer wired to the test store."""
84
+ return MemorySharer(store=store, identity="capauth:alice@skworld.io")
85
+
86
+
87
+ @pytest.fixture()
88
+ def receiver_store(tmp_path: Path) -> MemoryStore:
89
+ """Separate MemoryStore for the receiving agent."""
90
+ from skmemory.backends.file_backend import FileBackend
91
+
92
+ backend = FileBackend(base_path=tmp_path / "receiver-memories")
93
+ return MemoryStore(primary=backend)
94
+
95
+
96
+ class TestShareFilter:
97
+ """Tests for ShareFilter behavior."""
98
+
99
+ def test_empty_filter(self) -> None:
100
+ """Empty filter reports as empty."""
101
+ sf = ShareFilter()
102
+ assert sf.is_empty() is True
103
+
104
+ def test_filter_with_tags(self) -> None:
105
+ """Filter with tags is not empty."""
106
+ sf = ShareFilter(tags=["project"])
107
+ assert sf.is_empty() is False
108
+
109
+ def test_filter_with_ids(self) -> None:
110
+ """Filter with memory_ids is not empty."""
111
+ sf = ShareFilter(memory_ids=["abc"])
112
+ assert sf.is_empty() is False
113
+
114
+
115
+ class TestExportMemories:
116
+ """Tests for memory export/selection."""
117
+
118
+ def test_export_by_tags(self, sharer: MemorySharer) -> None:
119
+ """Export selects memories matching tags."""
120
+ sf = ShareFilter(tags=["project"])
121
+ bundle = sharer.export_memories(sf, recipient="capauth:bob@skworld.io")
122
+
123
+ assert bundle.memory_count >= 1
124
+ assert bundle.sharer == "capauth:alice@skworld.io"
125
+ assert bundle.recipient == "capauth:bob@skworld.io"
126
+ assert bundle.checksum != ""
127
+
128
+ def test_export_by_layer(self, sharer: MemorySharer) -> None:
129
+ """Export selects memories in specified layers."""
130
+ sf = ShareFilter(layers=[MemoryLayer.MID])
131
+ bundle = sharer.export_memories(sf)
132
+ assert bundle.memory_count >= 1
133
+
134
+ def test_export_with_intensity_filter(self, sharer: MemorySharer) -> None:
135
+ """Intensity filter excludes low-intensity memories."""
136
+ sf = ShareFilter(tags=["daily", "project", "security"], min_intensity=4.0)
137
+ bundle = sharer.export_memories(sf)
138
+ for mem in bundle.memories:
139
+ assert mem.get("emotional", {}).get("intensity", 0) >= 4.0
140
+
141
+ def test_export_with_exclude_tags(self, sharer: MemorySharer) -> None:
142
+ """Exclude tags prevent certain memories from being shared."""
143
+ sf = ShareFilter(
144
+ tags=["project", "security"],
145
+ exclude_tags=["private"],
146
+ )
147
+ bundle = sharer.export_memories(sf)
148
+ for mem in bundle.memories:
149
+ assert "private" not in mem.get("tags", [])
150
+
151
+ def test_export_empty_filter_raises(self, sharer: MemorySharer) -> None:
152
+ """Empty filter raises ValueError for safety."""
153
+ sf = ShareFilter()
154
+ with pytest.raises(ValueError, match="Explicit criteria required"):
155
+ sharer.export_memories(sf)
156
+
157
+ def test_export_max_count(self, sharer: MemorySharer) -> None:
158
+ """Max count limits the number of exported memories."""
159
+ sf = ShareFilter(tags=["project", "daily", "security"], max_count=1)
160
+ bundle = sharer.export_memories(sf)
161
+ assert bundle.memory_count <= 1
162
+
163
+
164
+ class TestImportBundle:
165
+ """Tests for memory import."""
166
+
167
+ def test_import_adds_provenance(
168
+ self,
169
+ sharer: MemorySharer,
170
+ receiver_store: MemoryStore,
171
+ ) -> None:
172
+ """Imported memories have provenance tags."""
173
+ sf = ShareFilter(tags=["project"])
174
+ bundle = sharer.export_memories(sf)
175
+
176
+ receiver = MemorySharer(store=receiver_store, identity="capauth:bob@skworld.io")
177
+ result = receiver.import_bundle(bundle)
178
+
179
+ assert result["imported"] >= 1
180
+ assert result["errors"] == 0
181
+
182
+ imported = receiver_store.list_memories(tags=["shared"])
183
+ assert len(imported) >= 1
184
+ assert any("shared:from:capauth:alice@skworld.io" in m.tags for m in imported)
185
+
186
+ def test_import_untrusted_skips(
187
+ self, sharer: MemorySharer, receiver_store: MemoryStore
188
+ ) -> None:
189
+ """Untrusted sharer is rejected."""
190
+ sf = ShareFilter(tags=["project"])
191
+ bundle = sharer.export_memories(sf)
192
+
193
+ receiver = MemorySharer(store=receiver_store)
194
+ result = receiver.import_bundle(bundle, trust_sharer=False)
195
+ assert result["imported"] == 0
196
+ assert result["skipped"] == bundle.memory_count
197
+
198
+ def test_import_checksum_mismatch(self, receiver_store: MemoryStore) -> None:
199
+ """Tampered bundle fails checksum verification."""
200
+ bundle = ShareBundle(
201
+ sharer="evil",
202
+ memories=[{"title": "fake", "content": "hacked"}],
203
+ memory_count=1,
204
+ checksum="wrong_checksum",
205
+ )
206
+ receiver = MemorySharer(store=receiver_store)
207
+ result = receiver.import_bundle(bundle)
208
+ assert result["errors"] == 1
209
+
210
+
211
+ class TestEncryptDecrypt:
212
+ """Tests for PGP encryption of share bundles."""
213
+
214
+ def test_encrypt_decrypt_roundtrip(
215
+ self,
216
+ sharer: MemorySharer,
217
+ recipient_keys: tuple[str, str],
218
+ ) -> None:
219
+ """Bundle encrypted for recipient can be decrypted."""
220
+ priv, pub = recipient_keys
221
+ sf = ShareFilter(tags=["project"])
222
+ bundle = sharer.export_memories(sf)
223
+
224
+ encrypted = sharer.encrypt_bundle(bundle, pub)
225
+ assert encrypted.encrypted is True
226
+ assert len(encrypted.memories) == 1
227
+ assert "ciphertext" in encrypted.memories[0]
228
+
229
+ receiver = MemorySharer(store=sharer._store)
230
+ decrypted = receiver.decrypt_bundle(encrypted, priv, PASSPHRASE)
231
+ assert decrypted.encrypted is False
232
+ assert decrypted.memory_count == bundle.memory_count
233
+
234
+ def test_decrypt_plaintext_is_noop(self, sharer: MemorySharer) -> None:
235
+ """Decrypting a non-encrypted bundle returns it unchanged."""
236
+ sf = ShareFilter(tags=["project"])
237
+ bundle = sharer.export_memories(sf)
238
+
239
+ result = sharer.decrypt_bundle(bundle, "key", "pass")
240
+ assert result.encrypted is False
241
+
242
+
243
+ class TestBundlePersistence:
244
+ """Tests for save/load bundle files."""
245
+
246
+ def test_save_and_load(self, sharer: MemorySharer, tmp_path: Path) -> None:
247
+ """Bundle survives save/load roundtrip."""
248
+ sf = ShareFilter(tags=["project"])
249
+ bundle = sharer.export_memories(sf)
250
+
251
+ filepath = tmp_path / "bundle.json"
252
+ sharer.save_bundle(bundle, filepath)
253
+
254
+ loaded = MemorySharer.load_bundle(filepath)
255
+ assert loaded.bundle_id == bundle.bundle_id
256
+ assert loaded.memory_count == bundle.memory_count
257
+ assert loaded.checksum == bundle.checksum