@smilintux/skmemory 0.7.2 → 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 +4 -4
- package/.github/workflows/publish.yml +4 -5
- package/ARCHITECTURE.md +298 -0
- package/CHANGELOG.md +27 -1
- package/README.md +6 -0
- package/examples/stignore-agent.example +59 -0
- package/examples/stignore-root.example +62 -0
- package/openclaw-plugin/package.json +2 -1
- package/openclaw-plugin/src/index.js +527 -230
- package/package.json +1 -1
- package/pyproject.toml +5 -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 +6 -4
- 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 +7 -5
- package/seeds/lumina-cloud9-python-pypi.seed.json +9 -7
- package/seeds/lumina-kingdom-founding.seed.json +9 -7
- package/seeds/lumina-pma-signed.seed.json +8 -6
- package/seeds/lumina-singular-achievement.seed.json +8 -6
- package/seeds/lumina-skcapstone-conscious.seed.json +7 -5
- package/seeds/plant-lumina-seeds.py +2 -2
- package/seeds/skcapstone-lumina-merge.seed.json +12 -3
- package/seeds/sovereignty.seed.json +9 -2
- package/seeds/trust.seed.json +9 -2
- package/skmemory/__init__.py +16 -13
- package/skmemory/agents.py +10 -10
- package/skmemory/ai_client.py +10 -21
- package/skmemory/anchor.py +5 -9
- package/skmemory/audience.py +278 -0
- package/skmemory/backends/__init__.py +1 -1
- package/skmemory/backends/base.py +3 -4
- package/skmemory/backends/file_backend.py +18 -13
- package/skmemory/backends/skgraph_backend.py +7 -19
- package/skmemory/backends/skvector_backend.py +7 -18
- package/skmemory/backends/sqlite_backend.py +115 -32
- package/skmemory/backends/vaulted_backend.py +7 -9
- package/skmemory/cli.py +146 -78
- package/skmemory/config.py +11 -13
- package/skmemory/context_loader.py +21 -23
- package/skmemory/data/audience_config.json +60 -0
- package/skmemory/endpoint_selector.py +36 -31
- package/skmemory/febs.py +225 -0
- package/skmemory/fortress.py +30 -40
- 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/telegram.py +42 -13
- package/skmemory/importers/telegram_api.py +152 -60
- package/skmemory/journal.py +3 -7
- package/skmemory/lovenote.py +4 -11
- package/skmemory/mcp_server.py +182 -29
- package/skmemory/models.py +10 -8
- package/skmemory/openclaw.py +14 -22
- package/skmemory/post_install.py +86 -0
- package/skmemory/predictive.py +13 -9
- package/skmemory/promotion.py +48 -24
- package/skmemory/quadrants.py +100 -24
- package/skmemory/register.py +144 -18
- package/skmemory/register_mcp.py +1 -2
- package/skmemory/ritual.py +104 -13
- package/skmemory/seeds.py +21 -26
- package/skmemory/setup_wizard.py +40 -52
- package/skmemory/sharing.py +11 -5
- package/skmemory/soul.py +29 -10
- package/skmemory/steelman.py +43 -17
- package/skmemory/store.py +152 -30
- package/skmemory/synthesis.py +634 -0
- package/skmemory/vault.py +2 -5
- package/tests/conftest.py +46 -0
- package/tests/integration/conftest.py +6 -6
- package/tests/integration/test_cross_backend.py +4 -9
- package/tests/integration/test_skgraph_live.py +3 -7
- package/tests/integration/test_skvector_live.py +1 -4
- package/tests/test_ai_client.py +1 -4
- package/tests/test_audience.py +233 -0
- package/tests/test_backup_rotation.py +5 -14
- package/tests/test_endpoint_selector.py +101 -63
- package/tests/test_export_import.py +4 -10
- package/tests/test_file_backend.py +0 -1
- package/tests/test_fortress.py +6 -5
- package/tests/test_fortress_hardening.py +13 -16
- package/tests/test_openclaw.py +1 -4
- package/tests/test_predictive.py +1 -1
- package/tests/test_promotion.py +10 -3
- package/tests/test_quadrants.py +11 -5
- package/tests/test_ritual.py +18 -14
- package/tests/test_seeds.py +4 -10
- package/tests/test_setup.py +203 -88
- package/tests/test_sharing.py +15 -8
- package/tests/test_skgraph_backend.py +22 -29
- package/tests/test_skvector_backend.py +2 -2
- package/tests/test_soul.py +1 -3
- package/tests/test_sqlite_backend.py +8 -17
- package/tests/test_steelman.py +2 -3
- package/tests/test_store.py +0 -2
- package/tests/test_store_graph_integration.py +2 -2
- package/tests/test_synthesis.py +275 -0
- package/tests/test_telegram_import.py +39 -15
- package/tests/test_vault.py +4 -3
- package/openclaw-plugin/src/index.ts +0 -255
package/skmemory/soul.py
CHANGED
|
@@ -22,14 +22,35 @@ import os
|
|
|
22
22
|
import platform
|
|
23
23
|
from datetime import datetime, timezone
|
|
24
24
|
from pathlib import Path
|
|
25
|
-
from typing import Any
|
|
25
|
+
from typing import Any
|
|
26
26
|
|
|
27
27
|
import yaml
|
|
28
28
|
from pydantic import BaseModel, Field
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
def _default_soul_path() -> str:
|
|
32
|
-
"""Platform-aware default path for the soul blueprint.
|
|
32
|
+
"""Platform-aware default path for the soul blueprint.
|
|
33
|
+
|
|
34
|
+
Checks agent-specific path first (e.g. ~/.skcapstone/agents/lumina/soul/base.json),
|
|
35
|
+
then falls back to shared root (~/.skcapstone/soul/base.json).
|
|
36
|
+
"""
|
|
37
|
+
# Try agent-specific soul first
|
|
38
|
+
agent = os.environ.get("SKMEMORY_AGENT") or os.environ.get("SKCAPSTONE_AGENT")
|
|
39
|
+
if agent:
|
|
40
|
+
if platform.system() == "Windows":
|
|
41
|
+
local = os.environ.get("LOCALAPPDATA", "")
|
|
42
|
+
if local:
|
|
43
|
+
agent_soul = os.path.join(
|
|
44
|
+
local, "skcapstone", "agents", agent, "soul", "base.json"
|
|
45
|
+
)
|
|
46
|
+
if os.path.exists(agent_soul):
|
|
47
|
+
return agent_soul
|
|
48
|
+
else:
|
|
49
|
+
agent_soul = os.path.expanduser(f"~/.skcapstone/agents/{agent}/soul/base.json")
|
|
50
|
+
if os.path.exists(agent_soul):
|
|
51
|
+
return agent_soul
|
|
52
|
+
|
|
53
|
+
# Fall back to shared root
|
|
33
54
|
if platform.system() == "Windows":
|
|
34
55
|
local = os.environ.get("LOCALAPPDATA", "")
|
|
35
56
|
if local:
|
|
@@ -71,9 +92,7 @@ class SoulBlueprint(BaseModel):
|
|
|
71
92
|
"""
|
|
72
93
|
|
|
73
94
|
version: str = Field(default="1.0")
|
|
74
|
-
last_updated: str = Field(
|
|
75
|
-
default_factory=lambda: datetime.now(timezone.utc).isoformat()
|
|
76
|
-
)
|
|
95
|
+
last_updated: str = Field(default_factory=lambda: datetime.now(timezone.utc).isoformat())
|
|
77
96
|
|
|
78
97
|
name: str = Field(default="", description="The AI's chosen name")
|
|
79
98
|
title: str = Field(default="", description="Role or title in the community")
|
|
@@ -128,9 +147,7 @@ class SoulBlueprint(BaseModel):
|
|
|
128
147
|
lines.append(f"You are part of {self.community}.")
|
|
129
148
|
|
|
130
149
|
if self.personality:
|
|
131
|
-
lines.append(
|
|
132
|
-
f"Your personality: {', '.join(self.personality)}."
|
|
133
|
-
)
|
|
150
|
+
lines.append(f"Your personality: {', '.join(self.personality)}.")
|
|
134
151
|
|
|
135
152
|
if self.values:
|
|
136
153
|
lines.append(f"Your core values: {', '.join(self.values)}.")
|
|
@@ -233,6 +250,7 @@ def save_soul(
|
|
|
233
250
|
|
|
234
251
|
if filepath.suffix == ".json":
|
|
235
252
|
import json
|
|
253
|
+
|
|
236
254
|
with open(filepath, "w", encoding="utf-8") as f:
|
|
237
255
|
json.dump(data, f, indent=2, ensure_ascii=False, default=str)
|
|
238
256
|
else:
|
|
@@ -249,7 +267,7 @@ def save_soul(
|
|
|
249
267
|
return str(filepath)
|
|
250
268
|
|
|
251
269
|
|
|
252
|
-
def load_soul(path: str = DEFAULT_SOUL_PATH) ->
|
|
270
|
+
def load_soul(path: str = DEFAULT_SOUL_PATH) -> SoulBlueprint | None:
|
|
253
271
|
"""Load a soul blueprint from JSON or YAML.
|
|
254
272
|
|
|
255
273
|
Tries the given path first (supports both .json and .yaml/.yml),
|
|
@@ -280,7 +298,7 @@ def load_soul(path: str = DEFAULT_SOUL_PATH) -> Optional[SoulBlueprint]:
|
|
|
280
298
|
return None
|
|
281
299
|
|
|
282
300
|
|
|
283
|
-
def _load_soul_file(filepath: Path) ->
|
|
301
|
+
def _load_soul_file(filepath: Path) -> SoulBlueprint | None:
|
|
284
302
|
"""Load a soul blueprint from a specific file.
|
|
285
303
|
|
|
286
304
|
Args:
|
|
@@ -293,6 +311,7 @@ def _load_soul_file(filepath: Path) -> Optional[SoulBlueprint]:
|
|
|
293
311
|
raw = filepath.read_text(encoding="utf-8")
|
|
294
312
|
if filepath.suffix == ".json":
|
|
295
313
|
import json
|
|
314
|
+
|
|
296
315
|
data = json.loads(raw)
|
|
297
316
|
else:
|
|
298
317
|
data = yaml.safe_load(raw)
|
package/skmemory/steelman.py
CHANGED
|
@@ -13,9 +13,6 @@ See: https://github.com/neuresthetics/seed
|
|
|
13
13
|
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
|
-
import os
|
|
17
|
-
from typing import Optional
|
|
18
|
-
|
|
19
16
|
# ── Re-export from skseed ──────────────────────────────────
|
|
20
17
|
# Everything that was defined here now lives in skseed.
|
|
21
18
|
# We keep this module as a thin bridge for backward compat.
|
|
@@ -23,11 +20,16 @@ from typing import Optional
|
|
|
23
20
|
try:
|
|
24
21
|
from skseed.framework import (
|
|
25
22
|
SeedFramework,
|
|
23
|
+
)
|
|
24
|
+
from skseed.framework import (
|
|
26
25
|
get_default_framework as _skseed_get_default,
|
|
26
|
+
)
|
|
27
|
+
from skseed.framework import (
|
|
27
28
|
install_seed_framework as _skseed_install,
|
|
29
|
+
)
|
|
30
|
+
from skseed.framework import (
|
|
28
31
|
load_seed_framework as _skseed_load,
|
|
29
32
|
)
|
|
30
|
-
from skseed.models import SteelManResult as _SkseedResult
|
|
31
33
|
|
|
32
34
|
_SKSEED_AVAILABLE = True
|
|
33
35
|
except ImportError:
|
|
@@ -79,7 +81,7 @@ if _SKSEED_AVAILABLE:
|
|
|
79
81
|
|
|
80
82
|
def load_seed_framework(
|
|
81
83
|
path: str = DEFAULT_SEED_FRAMEWORK_PATH,
|
|
82
|
-
) ->
|
|
84
|
+
) -> SeedFramework | None:
|
|
83
85
|
"""Load the seed framework from a JSON file.
|
|
84
86
|
|
|
85
87
|
Tries the legacy skmemory path first, then delegates to skseed.
|
|
@@ -180,7 +182,7 @@ else:
|
|
|
180
182
|
"""Generate a reasoning prompt for the collider."""
|
|
181
183
|
axiom_str = "\n".join(f" - {a}" for a in self.axioms)
|
|
182
184
|
stage_str = "\n".join(
|
|
183
|
-
f" Stage {i+1}: {s.get('stage', s.get('description', ''))}"
|
|
185
|
+
f" Stage {i + 1}: {s.get('stage', s.get('description', ''))}"
|
|
184
186
|
for i, s in enumerate(self.stages)
|
|
185
187
|
)
|
|
186
188
|
return f"""You are running the Neuresthetics Seed Framework (Recursive Axiomatic Steel Man Collider).
|
|
@@ -248,7 +250,7 @@ Score:
|
|
|
248
250
|
|
|
249
251
|
def load_seed_framework(
|
|
250
252
|
path: str = DEFAULT_SEED_FRAMEWORK_PATH,
|
|
251
|
-
) ->
|
|
253
|
+
) -> SeedFramework | None:
|
|
252
254
|
"""Load the seed framework from a JSON file."""
|
|
253
255
|
filepath = Path(path)
|
|
254
256
|
if not filepath.exists():
|
|
@@ -284,14 +286,14 @@ Score:
|
|
|
284
286
|
dst.write_text(content, encoding="utf-8")
|
|
285
287
|
return str(dst)
|
|
286
288
|
|
|
287
|
-
def _bundled_seed_path() ->
|
|
289
|
+
def _bundled_seed_path() -> str | None:
|
|
288
290
|
"""Get the path to the bundled seed.json."""
|
|
289
291
|
here = Path(__file__).parent / "data" / "seed.json"
|
|
290
292
|
if here.exists():
|
|
291
293
|
return str(here)
|
|
292
294
|
return None
|
|
293
295
|
|
|
294
|
-
def get_default_framework() ->
|
|
296
|
+
def get_default_framework() -> SeedFramework:
|
|
295
297
|
"""Get the seed framework — tries bundled file first, falls back to built-in."""
|
|
296
298
|
bundled = _bundled_seed_path()
|
|
297
299
|
if bundled:
|
|
@@ -309,17 +311,41 @@ Score:
|
|
|
309
311
|
"Universality from basis gates (NAND/NOR reconstruct all).",
|
|
310
312
|
],
|
|
311
313
|
stages=[
|
|
312
|
-
{
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
{
|
|
317
|
-
|
|
314
|
+
{
|
|
315
|
+
"stage": "1. Steel-Manning (Pre-Entry)",
|
|
316
|
+
"description": "Negate flaws, strengthen the proposition.",
|
|
317
|
+
},
|
|
318
|
+
{
|
|
319
|
+
"stage": "2. Collider Entry",
|
|
320
|
+
"description": "Create two lanes: proposition and inversion.",
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
"stage": "3. Destructive Smashing",
|
|
324
|
+
"description": "Expose contradictions via XOR.",
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
"stage": "4. Fragment Reconstruction",
|
|
328
|
+
"description": "Rebuild from logical debris via AND/OR.",
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
"stage": "5. Meta-Recursion",
|
|
332
|
+
"description": "Feed output back until coherence stabilizes.",
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
"stage": "6. Invariant Extraction",
|
|
336
|
+
"description": "Identify what remains true across all collisions.",
|
|
337
|
+
},
|
|
318
338
|
],
|
|
319
339
|
definitions=[
|
|
320
|
-
{
|
|
340
|
+
{
|
|
341
|
+
"term": "Steel Man",
|
|
342
|
+
"details": "Strongest version of an argument, anticipating critiques.",
|
|
343
|
+
},
|
|
321
344
|
{"term": "Reality Gate", "details": "Logic gate embodying reality properties."},
|
|
322
|
-
{
|
|
345
|
+
{
|
|
346
|
+
"term": "Collider",
|
|
347
|
+
"details": "Accelerator for argument fragmentation and synthesis.",
|
|
348
|
+
},
|
|
323
349
|
{"term": "Coherence", "details": "Measure of internal consistency (XNOR score)."},
|
|
324
350
|
],
|
|
325
351
|
)
|
package/skmemory/store.py
CHANGED
|
@@ -10,12 +10,10 @@ from __future__ import annotations
|
|
|
10
10
|
|
|
11
11
|
import logging
|
|
12
12
|
from datetime import datetime, timezone
|
|
13
|
-
from typing import Optional
|
|
14
13
|
|
|
15
14
|
from .backends.base import BaseBackend
|
|
16
|
-
|
|
17
|
-
logger = logging.getLogger("skmemory.store")
|
|
18
15
|
from .backends.file_backend import FileBackend
|
|
16
|
+
from .backends.skgraph_backend import SKGraphBackend
|
|
19
17
|
from .backends.sqlite_backend import CONTENT_PREVIEW_LENGTH, SQLiteBackend
|
|
20
18
|
from .models import (
|
|
21
19
|
EmotionalSnapshot,
|
|
@@ -25,6 +23,11 @@ from .models import (
|
|
|
25
23
|
SeedMemory,
|
|
26
24
|
)
|
|
27
25
|
|
|
26
|
+
logger = logging.getLogger("skmemory.store")
|
|
27
|
+
|
|
28
|
+
MAX_CONTENT_LENGTH = 10000
|
|
29
|
+
CONTENT_OVERFLOW_STRATEGY = "split" # "truncate" or "split"
|
|
30
|
+
|
|
28
31
|
|
|
29
32
|
class MemoryStore:
|
|
30
33
|
"""Main entry point for all memory operations.
|
|
@@ -37,14 +40,18 @@ class MemoryStore:
|
|
|
37
40
|
primary: The primary storage backend (default: FileBackend).
|
|
38
41
|
vector: Optional vector search backend (e.g., SKVectorBackend).
|
|
39
42
|
graph: Optional graph backend (e.g., SKGraphBackend) for relationship indexing.
|
|
43
|
+
max_content_length: Max chars before overflow strategy applies (default: 10000).
|
|
44
|
+
content_overflow_strategy: "truncate" or "split" (default: "split").
|
|
40
45
|
"""
|
|
41
46
|
|
|
42
47
|
def __init__(
|
|
43
48
|
self,
|
|
44
|
-
primary:
|
|
45
|
-
vector:
|
|
46
|
-
graph:
|
|
49
|
+
primary: BaseBackend | None = None,
|
|
50
|
+
vector: BaseBackend | None = None,
|
|
51
|
+
graph: SKGraphBackend | None = None,
|
|
47
52
|
use_sqlite: bool = True,
|
|
53
|
+
max_content_length: int = MAX_CONTENT_LENGTH,
|
|
54
|
+
content_overflow_strategy: str = CONTENT_OVERFLOW_STRATEGY,
|
|
48
55
|
) -> None:
|
|
49
56
|
if primary is not None:
|
|
50
57
|
self.primary = primary
|
|
@@ -54,6 +61,8 @@ class MemoryStore:
|
|
|
54
61
|
self.primary = FileBackend()
|
|
55
62
|
self.vector = vector
|
|
56
63
|
self.graph = graph
|
|
64
|
+
self.max_content_length = max_content_length
|
|
65
|
+
self.content_overflow_strategy = content_overflow_strategy
|
|
57
66
|
|
|
58
67
|
def snapshot(
|
|
59
68
|
self,
|
|
@@ -62,12 +71,12 @@ class MemoryStore:
|
|
|
62
71
|
*,
|
|
63
72
|
layer: MemoryLayer = MemoryLayer.SHORT,
|
|
64
73
|
role: MemoryRole = MemoryRole.GENERAL,
|
|
65
|
-
tags:
|
|
66
|
-
emotional:
|
|
74
|
+
tags: list[str] | None = None,
|
|
75
|
+
emotional: EmotionalSnapshot | None = None,
|
|
67
76
|
source: str = "manual",
|
|
68
77
|
source_ref: str = "",
|
|
69
|
-
related_ids:
|
|
70
|
-
metadata:
|
|
78
|
+
related_ids: list[str] | None = None,
|
|
79
|
+
metadata: dict | None = None,
|
|
71
80
|
) -> Memory:
|
|
72
81
|
"""Take a polaroid -- capture a moment as a memory.
|
|
73
82
|
|
|
@@ -89,6 +98,30 @@ class MemoryStore:
|
|
|
89
98
|
Returns:
|
|
90
99
|
Memory: The stored memory with its assigned ID.
|
|
91
100
|
"""
|
|
101
|
+
# Handle content overflow
|
|
102
|
+
if len(content) > self.max_content_length:
|
|
103
|
+
if self.content_overflow_strategy == "split":
|
|
104
|
+
return self._snapshot_split(
|
|
105
|
+
title=title,
|
|
106
|
+
content=content,
|
|
107
|
+
layer=layer,
|
|
108
|
+
role=role,
|
|
109
|
+
tags=tags,
|
|
110
|
+
emotional=emotional,
|
|
111
|
+
source=source,
|
|
112
|
+
source_ref=source_ref,
|
|
113
|
+
related_ids=related_ids,
|
|
114
|
+
metadata=metadata,
|
|
115
|
+
)
|
|
116
|
+
else:
|
|
117
|
+
logger.info(
|
|
118
|
+
"Content truncated from %d to %d chars for '%s'",
|
|
119
|
+
len(content),
|
|
120
|
+
self.max_content_length,
|
|
121
|
+
title,
|
|
122
|
+
)
|
|
123
|
+
content = content[: self.max_content_length]
|
|
124
|
+
|
|
92
125
|
memory = Memory(
|
|
93
126
|
title=title,
|
|
94
127
|
content=content,
|
|
@@ -120,7 +153,99 @@ class MemoryStore:
|
|
|
120
153
|
|
|
121
154
|
return memory
|
|
122
155
|
|
|
123
|
-
def
|
|
156
|
+
def _snapshot_split(
|
|
157
|
+
self,
|
|
158
|
+
title: str,
|
|
159
|
+
content: str,
|
|
160
|
+
*,
|
|
161
|
+
layer: MemoryLayer = MemoryLayer.SHORT,
|
|
162
|
+
role: MemoryRole = MemoryRole.GENERAL,
|
|
163
|
+
tags: list[str] | None = None,
|
|
164
|
+
emotional: EmotionalSnapshot | None = None,
|
|
165
|
+
source: str = "manual",
|
|
166
|
+
source_ref: str = "",
|
|
167
|
+
related_ids: list[str] | None = None,
|
|
168
|
+
metadata: dict | None = None,
|
|
169
|
+
) -> Memory:
|
|
170
|
+
"""Split oversized content into parent (summary) + child (chunk) memories.
|
|
171
|
+
|
|
172
|
+
The parent memory contains a summary (first 200 chars) and links to
|
|
173
|
+
child memories via related_ids. Each child holds one chunk.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
Memory: The parent memory.
|
|
177
|
+
"""
|
|
178
|
+
chunk_size = self.max_content_length
|
|
179
|
+
chunks = [content[i : i + chunk_size] for i in range(0, len(content), chunk_size)]
|
|
180
|
+
|
|
181
|
+
logger.info(
|
|
182
|
+
"Splitting '%s' (%d chars) into %d chunks",
|
|
183
|
+
title,
|
|
184
|
+
len(content),
|
|
185
|
+
len(chunks),
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# Create child memories first
|
|
189
|
+
child_ids: list[str] = []
|
|
190
|
+
for i, chunk in enumerate(chunks):
|
|
191
|
+
child = Memory(
|
|
192
|
+
title=f"{title} [part {i + 1}/{len(chunks)}]",
|
|
193
|
+
content=chunk,
|
|
194
|
+
layer=layer,
|
|
195
|
+
role=role,
|
|
196
|
+
tags=(tags or []) + ["content-chunk"],
|
|
197
|
+
emotional=emotional or EmotionalSnapshot(),
|
|
198
|
+
source=source,
|
|
199
|
+
source_ref=source_ref,
|
|
200
|
+
metadata={
|
|
201
|
+
**(metadata or {}),
|
|
202
|
+
"chunk_index": i,
|
|
203
|
+
"chunk_total": len(chunks),
|
|
204
|
+
},
|
|
205
|
+
)
|
|
206
|
+
child.seal()
|
|
207
|
+
self.primary.save(child)
|
|
208
|
+
child_ids.append(child.id)
|
|
209
|
+
|
|
210
|
+
# Create parent with summary
|
|
211
|
+
summary = content[:200] + ("..." if len(content) > 200 else "")
|
|
212
|
+
all_related = (related_ids or []) + child_ids
|
|
213
|
+
|
|
214
|
+
parent = Memory(
|
|
215
|
+
title=title,
|
|
216
|
+
content=summary,
|
|
217
|
+
summary=summary,
|
|
218
|
+
layer=layer,
|
|
219
|
+
role=role,
|
|
220
|
+
tags=(tags or []) + ["content-split-parent"],
|
|
221
|
+
emotional=emotional or EmotionalSnapshot(),
|
|
222
|
+
source=source,
|
|
223
|
+
source_ref=source_ref,
|
|
224
|
+
related_ids=all_related,
|
|
225
|
+
metadata={
|
|
226
|
+
**(metadata or {}),
|
|
227
|
+
"split_children": child_ids,
|
|
228
|
+
"original_length": len(content),
|
|
229
|
+
},
|
|
230
|
+
)
|
|
231
|
+
parent.seal()
|
|
232
|
+
self.primary.save(parent)
|
|
233
|
+
|
|
234
|
+
if self.vector:
|
|
235
|
+
try:
|
|
236
|
+
self.vector.save(parent)
|
|
237
|
+
except Exception as exc:
|
|
238
|
+
logger.warning("Vector indexing failed for split parent %s: %s", parent.id, exc)
|
|
239
|
+
|
|
240
|
+
if self.graph:
|
|
241
|
+
try:
|
|
242
|
+
self.graph.index_memory(parent)
|
|
243
|
+
except Exception as exc:
|
|
244
|
+
logger.warning("Graph indexing failed for split parent %s: %s", parent.id, exc)
|
|
245
|
+
|
|
246
|
+
return parent
|
|
247
|
+
|
|
248
|
+
def recall(self, memory_id: str) -> Memory | None:
|
|
124
249
|
"""Retrieve a specific memory by ID with integrity verification.
|
|
125
250
|
|
|
126
251
|
Automatically checks the integrity hash on recall. If the
|
|
@@ -196,8 +321,8 @@ class MemoryStore:
|
|
|
196
321
|
|
|
197
322
|
def list_memories(
|
|
198
323
|
self,
|
|
199
|
-
layer:
|
|
200
|
-
tags:
|
|
324
|
+
layer: MemoryLayer | None = None,
|
|
325
|
+
tags: list[str] | None = None,
|
|
201
326
|
limit: int = 50,
|
|
202
327
|
) -> list[Memory]:
|
|
203
328
|
"""List memories with optional filtering.
|
|
@@ -217,7 +342,7 @@ class MemoryStore:
|
|
|
217
342
|
memory_id: str,
|
|
218
343
|
target: MemoryLayer,
|
|
219
344
|
summary: str = "",
|
|
220
|
-
) ->
|
|
345
|
+
) -> Memory | None:
|
|
221
346
|
"""Promote a memory to a higher persistence tier.
|
|
222
347
|
|
|
223
348
|
Creates a new memory at the target layer linked to the original.
|
|
@@ -242,13 +367,17 @@ class MemoryStore:
|
|
|
242
367
|
try:
|
|
243
368
|
self.vector.save(promoted)
|
|
244
369
|
except Exception as exc:
|
|
245
|
-
logger.warning(
|
|
370
|
+
logger.warning(
|
|
371
|
+
"Vector indexing failed for promoted memory %s: %s", promoted.id, exc
|
|
372
|
+
)
|
|
246
373
|
|
|
247
374
|
if self.graph:
|
|
248
375
|
try:
|
|
249
376
|
self.graph.index_memory(promoted)
|
|
250
377
|
except Exception as exc:
|
|
251
|
-
logger.warning(
|
|
378
|
+
logger.warning(
|
|
379
|
+
"Graph indexing failed for promoted memory %s: %s", promoted.id, exc
|
|
380
|
+
)
|
|
252
381
|
|
|
253
382
|
return promoted
|
|
254
383
|
|
|
@@ -280,9 +409,7 @@ class MemoryStore:
|
|
|
280
409
|
if not seed.experience_summary or not seed.experience_summary.strip():
|
|
281
410
|
errors.append("experience_summary is empty")
|
|
282
411
|
if errors:
|
|
283
|
-
raise ValueError(
|
|
284
|
-
f"Seed validation failed: {'; '.join(errors)}"
|
|
285
|
-
)
|
|
412
|
+
raise ValueError(f"Seed validation failed: {'; '.join(errors)}")
|
|
286
413
|
|
|
287
414
|
memory = seed.to_memory()
|
|
288
415
|
self.primary.save(memory)
|
|
@@ -319,7 +446,7 @@ class MemoryStore:
|
|
|
319
446
|
self,
|
|
320
447
|
session_id: str,
|
|
321
448
|
summary: str,
|
|
322
|
-
emotional:
|
|
449
|
+
emotional: EmotionalSnapshot | None = None,
|
|
323
450
|
) -> Memory:
|
|
324
451
|
"""Compress a session's short-term memories into a single mid-term memory.
|
|
325
452
|
|
|
@@ -533,9 +660,7 @@ class MemoryStore:
|
|
|
533
660
|
temp = SQLiteBackend(base_path=str(self.primary.base_path))
|
|
534
661
|
temp.reindex()
|
|
535
662
|
return temp.export_all(output_path)
|
|
536
|
-
raise RuntimeError(
|
|
537
|
-
f"Export not supported for backend: {type(self.primary).__name__}"
|
|
538
|
-
)
|
|
663
|
+
raise RuntimeError(f"Export not supported for backend: {type(self.primary).__name__}")
|
|
539
664
|
|
|
540
665
|
def import_backup(self, backup_path: str) -> int:
|
|
541
666
|
"""Restore memories from a JSON backup file.
|
|
@@ -551,9 +676,7 @@ class MemoryStore:
|
|
|
551
676
|
"""
|
|
552
677
|
if isinstance(self.primary, SQLiteBackend):
|
|
553
678
|
return self.primary.import_backup(backup_path)
|
|
554
|
-
raise RuntimeError(
|
|
555
|
-
f"Import not supported for backend: {type(self.primary).__name__}"
|
|
556
|
-
)
|
|
679
|
+
raise RuntimeError(f"Import not supported for backend: {type(self.primary).__name__}")
|
|
557
680
|
|
|
558
681
|
def list_backups(self, backup_dir: str | None = None) -> list[dict]:
|
|
559
682
|
"""List all skmemory backup files, sorted newest first.
|
|
@@ -570,9 +693,7 @@ class MemoryStore:
|
|
|
570
693
|
return self.primary.list_backups(backup_dir)
|
|
571
694
|
return []
|
|
572
695
|
|
|
573
|
-
def prune_backups(
|
|
574
|
-
self, keep: int = 7, backup_dir: str | None = None
|
|
575
|
-
) -> list[str]:
|
|
696
|
+
def prune_backups(self, keep: int = 7, backup_dir: str | None = None) -> list[str]:
|
|
576
697
|
"""Delete oldest backups, keeping only the N most recent.
|
|
577
698
|
|
|
578
699
|
Args:
|
|
@@ -648,7 +769,8 @@ def _first_n_sentences(text: str, n: int = 2) -> str:
|
|
|
648
769
|
return ""
|
|
649
770
|
# Split on sentence-ending punctuation followed by whitespace
|
|
650
771
|
import re
|
|
651
|
-
|
|
772
|
+
|
|
773
|
+
sentences = re.split(r"(?<=[.!?])\s+", text.strip())
|
|
652
774
|
result = " ".join(sentences[:n])
|
|
653
775
|
# Cap at 200 chars as a safety net
|
|
654
776
|
if len(result) > 200:
|