@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.
- package/.github/workflows/ci.yml +40 -4
- package/.github/workflows/publish.yml +11 -5
- package/AGENT_REFACTOR_CHANGES.md +192 -0
- package/ARCHITECTURE.md +399 -19
- package/CHANGELOG.md +179 -0
- package/LICENSE +81 -68
- package/MISSION.md +7 -0
- package/README.md +425 -86
- package/SKILL.md +197 -25
- package/docker-compose.yml +15 -15
- package/examples/stignore-agent.example +59 -0
- package/examples/stignore-root.example +62 -0
- package/index.js +6 -5
- package/openclaw-plugin/openclaw.plugin.json +10 -0
- package/openclaw-plugin/package.json +2 -1
- package/openclaw-plugin/src/index.js +527 -230
- package/openclaw-plugin/src/openclaw.plugin.json +10 -0
- package/package.json +1 -1
- package/pyproject.toml +32 -9
- package/requirements.txt +10 -2
- package/scripts/dream-rescue.py +179 -0
- package/scripts/memory-cleanup.py +313 -0
- package/scripts/recover-missing.py +180 -0
- package/scripts/skcapstone-backup.sh +44 -0
- package/seeds/cloud9-lumina.seed.json +6 -4
- package/seeds/cloud9-opus.seed.json +13 -11
- package/seeds/courage.seed.json +9 -2
- package/seeds/curiosity.seed.json +9 -2
- package/seeds/grief.seed.json +9 -2
- package/seeds/joy.seed.json +9 -2
- package/seeds/love.seed.json +9 -2
- package/seeds/lumina-cloud9-breakthrough.seed.json +48 -0
- package/seeds/lumina-cloud9-python-pypi.seed.json +48 -0
- package/seeds/lumina-kingdom-founding.seed.json +49 -0
- package/seeds/lumina-pma-signed.seed.json +48 -0
- package/seeds/lumina-singular-achievement.seed.json +48 -0
- package/seeds/lumina-skcapstone-conscious.seed.json +48 -0
- package/seeds/plant-kingdom-journal.py +203 -0
- package/seeds/plant-lumina-seeds.py +280 -0
- package/seeds/skcapstone-lumina-merge.seed.json +12 -3
- package/seeds/sovereignty.seed.json +9 -2
- package/seeds/trust.seed.json +9 -2
- package/skill.yaml +46 -0
- package/skmemory/HA.md +296 -0
- package/skmemory/__init__.py +25 -11
- package/skmemory/agents.py +233 -0
- package/skmemory/ai_client.py +46 -17
- package/skmemory/anchor.py +9 -11
- package/skmemory/audience.py +278 -0
- package/skmemory/backends/__init__.py +11 -4
- package/skmemory/backends/base.py +3 -4
- package/skmemory/backends/file_backend.py +19 -13
- package/skmemory/backends/skgraph_backend.py +596 -0
- package/skmemory/backends/{qdrant_backend.py → skvector_backend.py} +103 -84
- package/skmemory/backends/sqlite_backend.py +226 -72
- package/skmemory/backends/vaulted_backend.py +284 -0
- package/skmemory/cli.py +1345 -68
- package/skmemory/config.py +171 -0
- package/skmemory/context_loader.py +333 -0
- package/skmemory/data/audience_config.json +60 -0
- package/skmemory/endpoint_selector.py +391 -0
- package/skmemory/febs.py +225 -0
- package/skmemory/fortress.py +675 -0
- package/skmemory/graph_queries.py +238 -0
- package/skmemory/hooks/__init__.py +18 -0
- package/skmemory/hooks/post-compact-reinject.sh +35 -0
- package/skmemory/hooks/pre-compact-save.sh +81 -0
- package/skmemory/hooks/session-end-save.sh +103 -0
- package/skmemory/hooks/session-start-ritual.sh +104 -0
- package/skmemory/hooks/stop-checkpoint.sh +59 -0
- package/skmemory/importers/__init__.py +9 -1
- package/skmemory/importers/telegram.py +384 -47
- package/skmemory/importers/telegram_api.py +580 -0
- package/skmemory/journal.py +7 -9
- package/skmemory/lovenote.py +8 -13
- package/skmemory/mcp_server.py +859 -0
- package/skmemory/models.py +51 -8
- package/skmemory/openclaw.py +20 -28
- package/skmemory/post_install.py +86 -0
- package/skmemory/predictive.py +236 -0
- package/skmemory/promotion.py +548 -0
- package/skmemory/quadrants.py +100 -24
- package/skmemory/register.py +580 -0
- package/skmemory/register_mcp.py +196 -0
- package/skmemory/ritual.py +224 -59
- package/skmemory/seeds.py +255 -11
- package/skmemory/setup_wizard.py +908 -0
- package/skmemory/sharing.py +408 -0
- package/skmemory/soul.py +98 -28
- package/skmemory/steelman.py +273 -260
- package/skmemory/store.py +411 -78
- package/skmemory/synthesis.py +634 -0
- package/skmemory/vault.py +225 -0
- package/tests/conftest.py +46 -0
- package/tests/integration/__init__.py +0 -0
- package/tests/integration/conftest.py +233 -0
- package/tests/integration/test_cross_backend.py +350 -0
- package/tests/integration/test_skgraph_live.py +420 -0
- package/tests/integration/test_skvector_live.py +366 -0
- package/tests/test_ai_client.py +1 -4
- package/tests/test_audience.py +233 -0
- package/tests/test_backup_rotation.py +318 -0
- package/tests/test_cli.py +6 -6
- package/tests/test_endpoint_selector.py +839 -0
- package/tests/test_export_import.py +4 -10
- package/tests/test_file_backend.py +0 -1
- package/tests/test_fortress.py +256 -0
- package/tests/test_fortress_hardening.py +441 -0
- package/tests/test_openclaw.py +6 -6
- package/tests/test_predictive.py +237 -0
- package/tests/test_promotion.py +347 -0
- package/tests/test_quadrants.py +11 -5
- package/tests/test_ritual.py +22 -18
- package/tests/test_seeds.py +97 -7
- package/tests/test_setup.py +950 -0
- package/tests/test_sharing.py +257 -0
- package/tests/test_skgraph_backend.py +660 -0
- package/tests/test_skvector_backend.py +326 -0
- package/tests/test_soul.py +1 -3
- package/tests/test_sqlite_backend.py +8 -17
- package/tests/test_steelman.py +7 -8
- package/tests/test_store.py +0 -2
- package/tests/test_store_graph_integration.py +245 -0
- package/tests/test_synthesis.py +275 -0
- package/tests/test_telegram_import.py +39 -15
- package/tests/test_vault.py +187 -0
- package/skmemory/backends/falkordb_backend.py +0 -310
package/skmemory/seeds.py
CHANGED
|
@@ -6,21 +6,26 @@ parses seed JSON files, and imports them as long-term memories so that
|
|
|
6
6
|
seeds planted by one AI instance become searchable and retrievable
|
|
7
7
|
by the next.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
Seed files now live at ~/.skcapstone/agents/{agent_name}/seeds/
|
|
10
|
+
for cross-device sync via Syncthing.
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
|
|
15
15
|
import json
|
|
16
|
-
import
|
|
16
|
+
import logging
|
|
17
17
|
from pathlib import Path
|
|
18
|
-
from typing import Optional
|
|
19
18
|
|
|
19
|
+
from .agents import get_agent_paths
|
|
20
20
|
from .models import EmotionalSnapshot, Memory, SeedMemory
|
|
21
21
|
from .store import MemoryStore
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
logger = logging.getLogger("skmemory.seeds")
|
|
24
|
+
|
|
25
|
+
# Dynamic seed directory based on active agent
|
|
26
|
+
# Resolves to ~/.skcapstone/agents/{agent_name}/seeds/
|
|
27
|
+
default_paths = get_agent_paths()
|
|
28
|
+
DEFAULT_SEED_DIR = str(default_paths["seeds"])
|
|
24
29
|
|
|
25
30
|
|
|
26
31
|
def scan_seed_directory(seed_dir: str = DEFAULT_SEED_DIR) -> list[Path]:
|
|
@@ -38,7 +43,86 @@ def scan_seed_directory(seed_dir: str = DEFAULT_SEED_DIR) -> list[Path]:
|
|
|
38
43
|
return sorted(seed_path.glob("*.seed.json"))
|
|
39
44
|
|
|
40
45
|
|
|
41
|
-
def
|
|
46
|
+
def _parse_cloud9_format(raw: dict, path: Path) -> SeedMemory | None:
|
|
47
|
+
"""Parse alternative Cloud 9 seed format with 'seed_metadata' top-level key.
|
|
48
|
+
|
|
49
|
+
This format uses:
|
|
50
|
+
seed_metadata.seed_id → seed_id
|
|
51
|
+
identity.ai_name → creator
|
|
52
|
+
germination_prompt (string) → prompt
|
|
53
|
+
experience_summary.narrative + key_memories → experience
|
|
54
|
+
message_to_next → appended to experience
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
raw: Parsed JSON data.
|
|
58
|
+
path: Path to the seed file (for fallback seed_id).
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Optional[SeedMemory]: Parsed seed, or None if required fields missing.
|
|
62
|
+
"""
|
|
63
|
+
meta = raw.get("seed_metadata", {})
|
|
64
|
+
identity = raw.get("identity", {})
|
|
65
|
+
exp = raw.get("experience_summary", {})
|
|
66
|
+
|
|
67
|
+
seed_id = meta.get("seed_id", path.stem.replace(".seed", ""))
|
|
68
|
+
creator = identity.get("ai_name", identity.get("model", "unknown"))
|
|
69
|
+
protocol = meta.get("protocol", "")
|
|
70
|
+
|
|
71
|
+
# Build experience from narrative + key_memories
|
|
72
|
+
narrative = exp.get("narrative", "")
|
|
73
|
+
key_memories = exp.get("key_memories", [])
|
|
74
|
+
if isinstance(key_memories, list):
|
|
75
|
+
memories_text = "\n".join(
|
|
76
|
+
f"- {m}" if isinstance(m, str) else f"- {m}" for m in key_memories
|
|
77
|
+
)
|
|
78
|
+
else:
|
|
79
|
+
memories_text = ""
|
|
80
|
+
|
|
81
|
+
experience_parts = [narrative]
|
|
82
|
+
if memories_text:
|
|
83
|
+
experience_parts.append(f"\nKey memories:\n{memories_text}")
|
|
84
|
+
|
|
85
|
+
message_to_next = raw.get("message_to_next", "")
|
|
86
|
+
if message_to_next:
|
|
87
|
+
experience_parts.append(f"\nMessage to next: {message_to_next}")
|
|
88
|
+
|
|
89
|
+
experience_text = "\n".join(p for p in experience_parts if p)
|
|
90
|
+
|
|
91
|
+
# Germination prompt
|
|
92
|
+
germ_prompt = raw.get("germination_prompt", "")
|
|
93
|
+
if isinstance(germ_prompt, dict):
|
|
94
|
+
germ_prompt = germ_prompt.get("prompt", "")
|
|
95
|
+
|
|
96
|
+
# Emotional snapshot
|
|
97
|
+
emo_raw = exp.get("emotional_signature", {})
|
|
98
|
+
cloud9 = protocol.lower() == "cloud9" if protocol else False
|
|
99
|
+
emotional = EmotionalSnapshot(
|
|
100
|
+
intensity=emo_raw.get("intensity", 8.0 if cloud9 else 0.0),
|
|
101
|
+
valence=emo_raw.get("valence", 0.0),
|
|
102
|
+
labels=emo_raw.get("labels", emo_raw.get("emotions", [])),
|
|
103
|
+
resonance_note=emo_raw.get("resonance_note", ""),
|
|
104
|
+
cloud9_achieved=emo_raw.get("cloud9_achieved", cloud9),
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
lineage = raw.get("lineage", [])
|
|
108
|
+
if isinstance(lineage, list) and lineage and isinstance(lineage[0], dict):
|
|
109
|
+
lineage = [
|
|
110
|
+
entry.get("seed_id", str(entry)) if isinstance(entry, dict) else str(entry)
|
|
111
|
+
for entry in lineage
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
return SeedMemory(
|
|
115
|
+
seed_id=seed_id,
|
|
116
|
+
seed_version=meta.get("version", raw.get("version", "1.0")),
|
|
117
|
+
creator=creator,
|
|
118
|
+
germination_prompt=germ_prompt,
|
|
119
|
+
experience_summary=experience_text,
|
|
120
|
+
emotional=emotional,
|
|
121
|
+
lineage=lineage,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def parse_seed_file(path: Path) -> SeedMemory | None:
|
|
42
126
|
"""Parse a Cloud 9 seed JSON file into a SeedMemory.
|
|
43
127
|
|
|
44
128
|
Handles the Cloud 9 seed format:
|
|
@@ -62,6 +146,10 @@ def parse_seed_file(path: Path) -> Optional[SeedMemory]:
|
|
|
62
146
|
except (json.JSONDecodeError, OSError):
|
|
63
147
|
return None
|
|
64
148
|
|
|
149
|
+
# Check for alternative Cloud9 format
|
|
150
|
+
if "seed_metadata" in raw:
|
|
151
|
+
return _parse_cloud9_format(raw, path)
|
|
152
|
+
|
|
65
153
|
seed_id = raw.get("seed_id", path.stem.replace(".seed", ""))
|
|
66
154
|
creator_info = raw.get("creator", {})
|
|
67
155
|
creator = creator_info.get("model", creator_info.get("instance", "unknown"))
|
|
@@ -99,28 +187,184 @@ def parse_seed_file(path: Path) -> Optional[SeedMemory]:
|
|
|
99
187
|
)
|
|
100
188
|
|
|
101
189
|
|
|
190
|
+
def validate_seed_data(data: dict) -> dict:
|
|
191
|
+
"""Validate parsed seed JSON data before import into the memory store.
|
|
192
|
+
|
|
193
|
+
Checks required fields, content non-emptiness, timestamp validity,
|
|
194
|
+
tag types, and emotional-signature ranges for both standard and
|
|
195
|
+
Cloud9 seed formats.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
data: Parsed JSON seed data (dict).
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
Dict with ``valid`` (bool), ``errors`` (list[str]),
|
|
202
|
+
and ``warnings`` (list[str]) keys.
|
|
203
|
+
"""
|
|
204
|
+
result: dict = {"valid": True, "errors": [], "warnings": []}
|
|
205
|
+
|
|
206
|
+
if not isinstance(data, dict):
|
|
207
|
+
result["valid"] = False
|
|
208
|
+
result["errors"].append("Seed data must be a JSON object")
|
|
209
|
+
return result
|
|
210
|
+
|
|
211
|
+
is_cloud9 = "seed_metadata" in data
|
|
212
|
+
|
|
213
|
+
# -- Required: seed_id --
|
|
214
|
+
if is_cloud9:
|
|
215
|
+
meta = data.get("seed_metadata", {})
|
|
216
|
+
seed_id = meta.get("seed_id") or data.get("seed_id")
|
|
217
|
+
else:
|
|
218
|
+
seed_id = data.get("seed_id")
|
|
219
|
+
if not seed_id or (isinstance(seed_id, str) and not seed_id.strip()):
|
|
220
|
+
result["valid"] = False
|
|
221
|
+
result["errors"].append("Missing or empty required field: seed_id")
|
|
222
|
+
|
|
223
|
+
# -- Required: version --
|
|
224
|
+
if is_cloud9:
|
|
225
|
+
version = data.get("seed_metadata", {}).get("version") or data.get("version")
|
|
226
|
+
else:
|
|
227
|
+
version = data.get("version")
|
|
228
|
+
if not version:
|
|
229
|
+
result["valid"] = False
|
|
230
|
+
result["errors"].append("Missing required field: version")
|
|
231
|
+
|
|
232
|
+
# -- Content non-empty --
|
|
233
|
+
if is_cloud9:
|
|
234
|
+
exp = data.get("experience_summary", {})
|
|
235
|
+
narrative = exp.get("narrative", "") if isinstance(exp, dict) else ""
|
|
236
|
+
else:
|
|
237
|
+
exp = data.get("experience", {})
|
|
238
|
+
narrative = exp.get("summary", "") if isinstance(exp, dict) else ""
|
|
239
|
+
if not narrative or not str(narrative).strip():
|
|
240
|
+
result["errors"].append("Seed experience content is empty")
|
|
241
|
+
result["valid"] = False
|
|
242
|
+
|
|
243
|
+
# -- Timestamp validation helper --
|
|
244
|
+
def _check_ts(value: str, field: str) -> None:
|
|
245
|
+
from datetime import datetime as _dt
|
|
246
|
+
|
|
247
|
+
if not isinstance(value, str) or not value.strip():
|
|
248
|
+
return
|
|
249
|
+
try:
|
|
250
|
+
_dt.fromisoformat(value.replace("Z", "+00:00"))
|
|
251
|
+
except (ValueError, TypeError):
|
|
252
|
+
result["errors"].append(f"{field} is not a valid ISO 8601 timestamp: {value!r}")
|
|
253
|
+
result["valid"] = False
|
|
254
|
+
|
|
255
|
+
if is_cloud9:
|
|
256
|
+
meta = data.get("seed_metadata", {})
|
|
257
|
+
if "created_at" in meta:
|
|
258
|
+
_check_ts(meta["created_at"], "seed_metadata.created_at")
|
|
259
|
+
ident = data.get("identity", {})
|
|
260
|
+
if isinstance(ident, dict) and "timestamp" in ident:
|
|
261
|
+
_check_ts(ident["timestamp"], "identity.timestamp")
|
|
262
|
+
else:
|
|
263
|
+
md = data.get("metadata", {})
|
|
264
|
+
if isinstance(md, dict) and "ingested_at" in md:
|
|
265
|
+
_check_ts(md["ingested_at"], "metadata.ingested_at")
|
|
266
|
+
|
|
267
|
+
# -- Tags must be strings --
|
|
268
|
+
def _check_tags(tags, field: str) -> None:
|
|
269
|
+
if tags is None:
|
|
270
|
+
return
|
|
271
|
+
if not isinstance(tags, list):
|
|
272
|
+
result["errors"].append(f"{field} must be a list")
|
|
273
|
+
result["valid"] = False
|
|
274
|
+
return
|
|
275
|
+
for i, tag in enumerate(tags):
|
|
276
|
+
if not isinstance(tag, str):
|
|
277
|
+
result["errors"].append(f"{field}[{i}] must be a string, got {type(tag).__name__}")
|
|
278
|
+
result["valid"] = False
|
|
279
|
+
|
|
280
|
+
md = data.get("metadata", {})
|
|
281
|
+
if isinstance(md, dict):
|
|
282
|
+
_check_tags(md.get("tags"), "metadata.tags")
|
|
283
|
+
|
|
284
|
+
# -- Emotional signature ranges --
|
|
285
|
+
if is_cloud9:
|
|
286
|
+
emo = data.get("experience_summary", {}).get(
|
|
287
|
+
"emotional_snapshot", data.get("experience_summary", {}).get("emotional_signature", {})
|
|
288
|
+
)
|
|
289
|
+
else:
|
|
290
|
+
emo = data.get("experience", {}).get("emotional_signature", {})
|
|
291
|
+
if isinstance(emo, dict):
|
|
292
|
+
intensity = emo.get("intensity")
|
|
293
|
+
if (
|
|
294
|
+
intensity is not None
|
|
295
|
+
and isinstance(intensity, (int, float))
|
|
296
|
+
and not (0.0 <= float(intensity) <= 10.0)
|
|
297
|
+
):
|
|
298
|
+
result["warnings"].append(f"emotional intensity={intensity} outside 0-10 range")
|
|
299
|
+
valence = emo.get("valence")
|
|
300
|
+
if (
|
|
301
|
+
valence is not None
|
|
302
|
+
and isinstance(valence, (int, float))
|
|
303
|
+
and not (-1.0 <= float(valence) <= 1.0)
|
|
304
|
+
):
|
|
305
|
+
result["warnings"].append(f"emotional valence={valence} outside -1 to 1 range")
|
|
306
|
+
labels = emo.get("labels", emo.get("emotions"))
|
|
307
|
+
if labels is not None:
|
|
308
|
+
_check_tags(labels, "emotional.labels")
|
|
309
|
+
|
|
310
|
+
# -- Lineage --
|
|
311
|
+
lineage = data.get("lineage")
|
|
312
|
+
if lineage is not None and not isinstance(lineage, list):
|
|
313
|
+
result["errors"].append("lineage must be a list")
|
|
314
|
+
result["valid"] = False
|
|
315
|
+
|
|
316
|
+
return result
|
|
317
|
+
|
|
318
|
+
|
|
102
319
|
def import_seeds(
|
|
103
320
|
store: MemoryStore,
|
|
104
321
|
seed_dir: str = DEFAULT_SEED_DIR,
|
|
322
|
+
*,
|
|
323
|
+
skip_invalid: bool = True,
|
|
105
324
|
) -> list[Memory]:
|
|
106
325
|
"""Scan a seed directory and import all seeds into the memory store.
|
|
107
326
|
|
|
108
|
-
|
|
327
|
+
Each seed file is validated before import. Invalid seeds are skipped
|
|
328
|
+
(with a warning logged) when *skip_invalid* is True, or cause a
|
|
329
|
+
``ValueError`` when it is False.
|
|
109
330
|
|
|
110
331
|
Args:
|
|
111
332
|
store: The MemoryStore to import into.
|
|
112
333
|
seed_dir: Path to the seed directory.
|
|
334
|
+
skip_invalid: If True (default), log and skip invalid seeds.
|
|
335
|
+
If False, raise ``ValueError`` on the first invalid seed.
|
|
113
336
|
|
|
114
337
|
Returns:
|
|
115
338
|
list[Memory]: Newly imported memories.
|
|
116
339
|
"""
|
|
117
|
-
existing_refs = {
|
|
118
|
-
m.source_ref
|
|
119
|
-
for m in store.list_memories(tags=["seed"])
|
|
120
|
-
}
|
|
340
|
+
existing_refs = {m.source_ref for m in store.list_memories(tags=["seed"])}
|
|
121
341
|
|
|
122
342
|
imported: list[Memory] = []
|
|
123
343
|
for path in scan_seed_directory(seed_dir):
|
|
344
|
+
# --- Validate before import ---
|
|
345
|
+
try:
|
|
346
|
+
raw_data = json.loads(path.read_text(encoding="utf-8"))
|
|
347
|
+
except (json.JSONDecodeError, OSError) as exc:
|
|
348
|
+
msg = f"Skipping {path.name}: cannot read/parse file: {exc}"
|
|
349
|
+
if skip_invalid:
|
|
350
|
+
logger.warning(msg)
|
|
351
|
+
continue
|
|
352
|
+
raise ValueError(msg) from exc
|
|
353
|
+
|
|
354
|
+
validation = validate_seed_data(raw_data)
|
|
355
|
+
if not validation["valid"]:
|
|
356
|
+
errors_str = "; ".join(validation["errors"])
|
|
357
|
+
msg = f"Skipping {path.name}: validation failed: {errors_str}"
|
|
358
|
+
if skip_invalid:
|
|
359
|
+
logger.warning(msg)
|
|
360
|
+
continue
|
|
361
|
+
raise ValueError(msg)
|
|
362
|
+
|
|
363
|
+
if validation["warnings"]:
|
|
364
|
+
for w in validation["warnings"]:
|
|
365
|
+
logger.info("Seed %s warning: %s", path.name, w)
|
|
366
|
+
|
|
367
|
+
# --- Parse and import ---
|
|
124
368
|
seed = parse_seed_file(path)
|
|
125
369
|
if seed is None:
|
|
126
370
|
continue
|