@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
@@ -1,12 +1,12 @@
1
1
  """
2
- Qdrant vector search backend (Level 2).
2
+ SKVector — semantic vector search backend (Level 1).
3
3
 
4
- Enables semantic memory recall: instead of exact text matching,
5
- find memories by *meaning*. "That conversation where we felt connected"
6
- finds the right memory even if those exact words aren't in it.
4
+ Powered by Qdrant. Enables semantic memory recall: instead of exact text
5
+ matching, find memories by *meaning*. "That conversation where we felt
6
+ connected" finds the right memory even if those exact words aren't in it.
7
7
 
8
8
  Requires:
9
- pip install qdrant-client sentence-transformers
9
+ pip install skmemory[skvector]
10
10
 
11
11
  Qdrant free tier: 1GB storage, 256MB RAM -- enough for thousands of memories.
12
12
  SaaS endpoint: https://cloud.qdrant.io (free cluster available).
@@ -14,9 +14,8 @@ SaaS endpoint: https://cloud.qdrant.io (free cluster available).
14
14
 
15
15
  from __future__ import annotations
16
16
 
17
- import json
17
+ import hashlib
18
18
  import logging
19
- from typing import Optional
20
19
 
21
20
  from ..models import Memory, MemoryLayer
22
21
  from .base import BaseBackend
@@ -28,15 +27,36 @@ EMBEDDING_MODEL = "all-MiniLM-L6-v2"
28
27
  VECTOR_DIM = 384
29
28
 
30
29
 
31
- class QdrantBackend(BaseBackend):
32
- """Qdrant-powered semantic memory search.
30
+ def _extract_status_code(exc: Exception, unexpected_cls: type | None) -> int | None:
31
+ """Pull an HTTP status code from a qdrant-client exception.
33
32
 
34
- Stores memory embeddings in Qdrant for vector similarity search.
35
- Falls back gracefully if Qdrant or the embedding model is unavailable.
33
+ Works across qdrant-client versions: checks ``status_code`` attr first,
34
+ then falls back to the string representation for patterns like ``401``.
35
+ """
36
+ code = getattr(exc, "status_code", None)
37
+ if code is not None:
38
+ return int(code)
39
+ if unexpected_cls is not None and isinstance(exc, unexpected_cls):
40
+ code = getattr(exc, "status_code", None)
41
+ if code is not None:
42
+ return int(code)
43
+ text = str(exc)
44
+ for candidate in (401, 403):
45
+ if str(candidate) in text:
46
+ return candidate
47
+ return None
48
+
49
+
50
+ class SKVectorBackend(BaseBackend):
51
+ """SKVector — semantic memory search (powered by Qdrant).
52
+
53
+ Stores memory embeddings for vector similarity search.
54
+ Falls back gracefully if the vector engine or the embedding model
55
+ is unavailable.
36
56
 
37
57
  Args:
38
- url: Qdrant server URL (default: localhost:6333).
39
- api_key: API key for Qdrant Cloud.
58
+ url: SKVector server URL (default: localhost:6333).
59
+ api_key: API key for cloud-hosted SKVector.
40
60
  collection: Collection name (default: 'skmemory').
41
61
  embedding_model: Sentence-transformers model name.
42
62
  """
@@ -44,7 +64,7 @@ class QdrantBackend(BaseBackend):
44
64
  def __init__(
45
65
  self,
46
66
  url: str = "http://localhost:6333",
47
- api_key: Optional[str] = None,
67
+ api_key: str | None = None,
48
68
  collection: str = COLLECTION_NAME,
49
69
  embedding_model: str = EMBEDDING_MODEL,
50
70
  ) -> None:
@@ -55,6 +75,7 @@ class QdrantBackend(BaseBackend):
55
75
  self._client = None
56
76
  self._embedder = None
57
77
  self._initialized = False
78
+ self._last_error: str | None = None
58
79
 
59
80
  def _ensure_initialized(self) -> bool:
60
81
  """Lazy-initialize Qdrant client and embedding model.
@@ -69,18 +90,22 @@ class QdrantBackend(BaseBackend):
69
90
  from qdrant_client import QdrantClient
70
91
  from qdrant_client.models import Distance, VectorParams
71
92
  except ImportError:
72
- logger.warning("qdrant-client not installed: pip install qdrant-client")
93
+ logger.warning("qdrant-client not installed: pip install skmemory[skvector]")
73
94
  return False
74
95
 
75
96
  try:
76
97
  from sentence_transformers import SentenceTransformer
77
98
  except ImportError:
78
- logger.warning(
79
- "sentence-transformers not installed: "
80
- "pip install sentence-transformers"
81
- )
99
+ logger.warning("sentence-transformers not installed: pip install skmemory[skvector]")
82
100
  return False
83
101
 
102
+ try:
103
+ from qdrant_client.http.exceptions import (
104
+ UnexpectedResponse,
105
+ )
106
+ except ImportError:
107
+ UnexpectedResponse = None
108
+
84
109
  try:
85
110
  self._client = QdrantClient(url=self.url, api_key=self.api_key)
86
111
  collections = [c.name for c in self._client.get_collections().collections]
@@ -100,7 +125,20 @@ class QdrantBackend(BaseBackend):
100
125
  return True
101
126
 
102
127
  except Exception as e:
103
- logger.warning("Qdrant initialization failed: %s", e)
128
+ status = _extract_status_code(e, UnexpectedResponse)
129
+ if status in (401, 403):
130
+ hint = (
131
+ "SKVector authentication failed (HTTP %d). "
132
+ "Check your API key:\n"
133
+ " - CLI: --skvector-key YOUR_KEY\n"
134
+ " - Env: SKMEMORY_SKVECTOR_KEY=YOUR_KEY\n"
135
+ " - Code: SKVectorBackend(url=..., api_key='YOUR_KEY')"
136
+ )
137
+ logger.error(hint, status)
138
+ self._last_error = hint % status
139
+ else:
140
+ logger.warning("SKVector initialization failed: %s", e)
141
+ self._last_error = str(e)
104
142
  return False
105
143
 
106
144
  def _embed(self, text: str) -> list[float]:
@@ -155,8 +193,11 @@ class QdrantBackend(BaseBackend):
155
193
  if not embedding:
156
194
  return memory.id
157
195
 
196
+ # Use memory.id hash as Qdrant point ID (not content_hash which
197
+ # would collide if two memories have identical content).
198
+ point_id = int(hashlib.sha256(memory.id.encode()).hexdigest()[:15], 16)
158
199
  point = PointStruct(
159
- id=memory.content_hash(),
200
+ id=point_id,
160
201
  vector=embedding,
161
202
  payload=self._memory_to_payload(memory),
162
203
  )
@@ -167,7 +208,11 @@ class QdrantBackend(BaseBackend):
167
208
  )
168
209
  return memory.id
169
210
 
170
- def load(self, memory_id: str) -> Optional[Memory]:
211
+ def _id_to_point_id(self, memory_id: str) -> int:
212
+ """Convert a memory ID string to a deterministic Qdrant integer point ID."""
213
+ return int(hashlib.sha256(memory_id.encode()).hexdigest()[:15], 16)
214
+
215
+ def load(self, memory_id: str) -> Memory | None:
171
216
  """Retrieve a memory by ID from Qdrant.
172
217
 
173
218
  Args:
@@ -175,81 +220,56 @@ class QdrantBackend(BaseBackend):
175
220
 
176
221
  Returns:
177
222
  Optional[Memory]: The memory if found.
178
-
179
- Note:
180
- Qdrant uses content hashes as point IDs, so this does
181
- a scroll+filter. For direct ID lookup, use the file backend.
182
223
  """
183
224
  if not self._ensure_initialized():
184
225
  return None
185
226
 
186
- from qdrant_client.models import FieldCondition, Filter, MatchValue
187
-
188
- results = self._client.scroll(
189
- collection_name=self.collection,
190
- scroll_filter=Filter(
191
- must=[
192
- FieldCondition(
193
- key="memory_json",
194
- match=MatchValue(value=memory_id),
195
- )
196
- ]
197
- ),
198
- limit=1,
199
- )
200
-
201
- points = results[0] if results else []
202
- if not points:
203
- return None
204
-
205
227
  try:
228
+ points = self._client.retrieve(
229
+ collection_name=self.collection,
230
+ ids=[self._id_to_point_id(memory_id)],
231
+ with_payload=True,
232
+ )
233
+ if not points:
234
+ return None
206
235
  return Memory.model_validate_json(points[0].payload["memory_json"])
207
236
  except Exception:
208
237
  return None
209
238
 
210
239
  def delete(self, memory_id: str) -> bool:
211
- """Remove a memory from Qdrant by scrolling for it.
240
+ """Remove a memory from Qdrant by its deterministic point ID.
241
+
242
+ Returns False if the memory was not found.
212
243
 
213
244
  Args:
214
245
  memory_id: The memory identifier.
215
246
 
216
247
  Returns:
217
- bool: True if something was deleted.
248
+ bool: True if the memory existed and was deleted, False otherwise.
218
249
  """
219
250
  if not self._ensure_initialized():
220
251
  return False
221
252
 
222
- from qdrant_client.models import FieldCondition, Filter, MatchValue
223
-
224
- # Reason: Qdrant doesn't support delete-by-payload natively,
225
- # so we scroll to find the point ID then delete by point ID.
226
- results = self._client.scroll(
227
- collection_name=self.collection,
228
- scroll_filter=Filter(
229
- must=[
230
- FieldCondition(
231
- key="memory_json",
232
- match=MatchValue(value=memory_id),
233
- )
234
- ]
235
- ),
236
- limit=1,
237
- )
238
-
239
- points = results[0] if results else []
240
- if not points:
253
+ try:
254
+ points = self._client.retrieve(
255
+ collection_name=self.collection,
256
+ ids=[self._id_to_point_id(memory_id)],
257
+ with_payload=False,
258
+ )
259
+ if not points:
260
+ return False
261
+ self._client.delete(
262
+ collection_name=self.collection,
263
+ points_selector=[self._id_to_point_id(memory_id)],
264
+ )
265
+ return True
266
+ except Exception:
241
267
  return False
242
268
 
243
- self._client.delete(
244
- collection_name=self.collection,
245
- points_selector=[points[0].id],
246
- )
247
- return True
248
-
249
269
  def list_memories(
250
270
  self,
251
- layer: Optional[MemoryLayer] = None,
252
- tags: Optional[list[str]] = None,
271
+ layer: MemoryLayer | None = None,
272
+ tags: list[str] | None = None,
253
273
  limit: int = 50,
254
274
  ) -> list[Memory]:
255
275
  """List memories from Qdrant with filtering.
@@ -274,9 +294,7 @@ class QdrantBackend(BaseBackend):
274
294
  )
275
295
  if tags:
276
296
  for tag in tags:
277
- must_conditions.append(
278
- FieldCondition(key="tags", match=MatchValue(value=tag))
279
- )
297
+ must_conditions.append(FieldCondition(key="tags", match=MatchValue(value=tag)))
280
298
 
281
299
  scroll_filter = Filter(must=must_conditions) if must_conditions else None
282
300
 
@@ -324,9 +342,7 @@ class QdrantBackend(BaseBackend):
324
342
  memories = []
325
343
  for scored_point in results:
326
344
  try:
327
- mem = Memory.model_validate_json(
328
- scored_point.payload["memory_json"]
329
- )
345
+ mem = Memory.model_validate_json(scored_point.payload["memory_json"])
330
346
  memories.append(mem)
331
347
  except Exception:
332
348
  continue
@@ -340,17 +356,20 @@ class QdrantBackend(BaseBackend):
340
356
  dict: Status with connection and collection info.
341
357
  """
342
358
  if not self._ensure_initialized():
359
+ error_msg = self._last_error or (
360
+ "Not initialized (missing dependencies or connection failed)"
361
+ )
343
362
  return {
344
363
  "ok": False,
345
- "backend": "QdrantBackend",
346
- "error": "Not initialized (missing dependencies or connection failed)",
364
+ "backend": "SKVectorBackend",
365
+ "error": error_msg,
347
366
  }
348
367
 
349
368
  try:
350
369
  info = self._client.get_collection(self.collection)
351
370
  return {
352
371
  "ok": True,
353
- "backend": "QdrantBackend",
372
+ "backend": "SKVectorBackend",
354
373
  "url": self.url,
355
374
  "collection": self.collection,
356
375
  "points_count": info.points_count,
@@ -359,6 +378,6 @@ class QdrantBackend(BaseBackend):
359
378
  except Exception as e:
360
379
  return {
361
380
  "ok": False,
362
- "backend": "QdrantBackend",
381
+ "backend": "SKVectorBackend",
363
382
  "error": str(e),
364
383
  }