@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/skill.yaml
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
name: skmemory
|
|
2
|
+
version: "0.6.0"
|
|
3
|
+
description: >
|
|
4
|
+
Persistent sovereign memory layer. Short-term, mid-term, and long-term
|
|
5
|
+
stores with HMAC integrity seals and automatic promotion. Backends for
|
|
6
|
+
file, SQLite, vector, and graph storage.
|
|
7
|
+
|
|
8
|
+
author:
|
|
9
|
+
name: smilinTux.org
|
|
10
|
+
email: hello@smilintux.org
|
|
11
|
+
|
|
12
|
+
tags: [memory, persistence, sovereign, integrity, sqlite, vector]
|
|
13
|
+
category: core
|
|
14
|
+
|
|
15
|
+
tools:
|
|
16
|
+
- name: memory_store
|
|
17
|
+
description: Store a new memory with tags and importance score
|
|
18
|
+
entrypoint: skmemory.skill:memory_store
|
|
19
|
+
|
|
20
|
+
- name: memory_search
|
|
21
|
+
description: Full-text search across all memory layers
|
|
22
|
+
entrypoint: skmemory.skill:memory_search
|
|
23
|
+
|
|
24
|
+
- name: memory_recall
|
|
25
|
+
description: Recall a specific memory by ID
|
|
26
|
+
entrypoint: skmemory.skill:memory_recall
|
|
27
|
+
|
|
28
|
+
- name: memory_list
|
|
29
|
+
description: List memories filtered by layer, tags, or importance
|
|
30
|
+
entrypoint: skmemory.skill:memory_list
|
|
31
|
+
|
|
32
|
+
- name: memory_forget
|
|
33
|
+
description: Delete a memory by ID
|
|
34
|
+
entrypoint: skmemory.skill:memory_forget
|
|
35
|
+
|
|
36
|
+
- name: memory_promote
|
|
37
|
+
description: Promote a memory to a higher persistence layer
|
|
38
|
+
entrypoint: skmemory.skill:memory_promote
|
|
39
|
+
|
|
40
|
+
- name: memory_health
|
|
41
|
+
description: Check memory store health and statistics
|
|
42
|
+
entrypoint: skmemory.skill:memory_health
|
|
43
|
+
|
|
44
|
+
- name: memory_verify
|
|
45
|
+
description: Verify HMAC integrity seals on stored memories
|
|
46
|
+
entrypoint: skmemory.skill:memory_verify
|
package/skmemory/HA.md
ADDED
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
# SKMemory High Availability & Routing for SKVector and SKGraph Backends
|
|
2
|
+
|
|
3
|
+
> Self-contained endpoint routing for SKVector and SKGraph backends.
|
|
4
|
+
> No external load balancer. No new dependencies. Backward compatible.
|
|
5
|
+
|
|
6
|
+
## Overview
|
|
7
|
+
|
|
8
|
+
SKMemory's SKVector and SKGraph backends can run on multiple nodes across a
|
|
9
|
+
Tailscale mesh (or any network). The **EndpointSelector** sits between config
|
|
10
|
+
resolution and backend construction: it discovers endpoints, probes their
|
|
11
|
+
latency, selects the fastest healthy one, and fails over automatically.
|
|
12
|
+
|
|
13
|
+
Key properties:
|
|
14
|
+
- **On-demand probing** with TTL cache (no background threads)
|
|
15
|
+
- **Config endpoints take precedence** over heartbeat discovery
|
|
16
|
+
- **Graceful degradation** — missing heartbeats, Tailscale, or config all fail silently
|
|
17
|
+
- **Backward compatible** — single `skvector_url` configs work unchanged
|
|
18
|
+
|
|
19
|
+
## Architecture
|
|
20
|
+
|
|
21
|
+
### Routing Layer Diagram
|
|
22
|
+
|
|
23
|
+
```mermaid
|
|
24
|
+
graph TB
|
|
25
|
+
CLI[skmemory CLI / Agent] --> ES[EndpointSelector]
|
|
26
|
+
ES --> |probe| EP1[SKVector @ home:6333]
|
|
27
|
+
ES --> |probe| EP2[SKVector @ vps:6333]
|
|
28
|
+
ES --> |probe| EP3[SKGraph @ home:6379]
|
|
29
|
+
ES --> |probe| EP4[SKGraph @ cloud:6379]
|
|
30
|
+
ES --> |select best| MS[MemoryStore]
|
|
31
|
+
MS --> SQLite[SQLite Primary]
|
|
32
|
+
MS --> SKVectorBackend[SKVectorBackend]
|
|
33
|
+
MS --> SKGraphBackend[SKGraphBackend]
|
|
34
|
+
HB[Heartbeat Mesh] -.->|discover| ES
|
|
35
|
+
|
|
36
|
+
style ES fill:#f9f,stroke:#333
|
|
37
|
+
style MS fill:#bbf,stroke:#333
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The selector picks a URL, then backends are created with that URL. Backend
|
|
41
|
+
internals are **never modified** — the selector is a pure URL resolver.
|
|
42
|
+
|
|
43
|
+
### Endpoint Selection Flowchart
|
|
44
|
+
|
|
45
|
+
```mermaid
|
|
46
|
+
flowchart TD
|
|
47
|
+
A[Load config.yaml] --> B{Multi-endpoint<br/>or heartbeat_discovery?}
|
|
48
|
+
B -->|No| C[Use single URL as-is]
|
|
49
|
+
B -->|Yes| D[Build EndpointSelector]
|
|
50
|
+
D --> E{Heartbeat<br/>discovery enabled?}
|
|
51
|
+
E -->|Yes| F[Read ~/.skcapstone/heartbeats/*.json]
|
|
52
|
+
F --> G[Merge discovered endpoints]
|
|
53
|
+
E -->|No| G
|
|
54
|
+
G --> H{Probe results<br/>stale? >30s}
|
|
55
|
+
H -->|Yes| I[TCP probe all endpoints]
|
|
56
|
+
H -->|No| J[Use cached results]
|
|
57
|
+
I --> K[Apply routing strategy]
|
|
58
|
+
J --> K
|
|
59
|
+
K --> L[Return best URL]
|
|
60
|
+
L --> M[Create backend with URL]
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Failover Sequence
|
|
64
|
+
|
|
65
|
+
```mermaid
|
|
66
|
+
sequenceDiagram
|
|
67
|
+
participant CLI as skmemory CLI
|
|
68
|
+
participant ES as EndpointSelector
|
|
69
|
+
participant A as SKVector @ home:6333
|
|
70
|
+
participant B as SKVector @ vps:6333
|
|
71
|
+
|
|
72
|
+
CLI->>ES: select_skvector()
|
|
73
|
+
ES->>A: TCP probe (port 6333)
|
|
74
|
+
A--xES: Connection refused
|
|
75
|
+
Note over ES: fail_count++ (1/3)
|
|
76
|
+
ES->>B: TCP probe (port 6333)
|
|
77
|
+
B-->>ES: Connected (12ms)
|
|
78
|
+
ES-->>CLI: vps:6333 (healthy)
|
|
79
|
+
|
|
80
|
+
Note over CLI: Later, home comes back...
|
|
81
|
+
CLI->>ES: select_skvector()
|
|
82
|
+
ES->>A: TCP probe (port 6333)
|
|
83
|
+
A-->>ES: Connected (2ms)
|
|
84
|
+
Note over ES: fail_count reset to 0
|
|
85
|
+
ES-->>CLI: home:6333 (lowest latency)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Heartbeat Discovery Flow
|
|
89
|
+
|
|
90
|
+
```mermaid
|
|
91
|
+
sequenceDiagram
|
|
92
|
+
participant Peer as Agent @ VPS
|
|
93
|
+
participant FS as Syncthing
|
|
94
|
+
participant Local as Local Agent
|
|
95
|
+
participant ES as EndpointSelector
|
|
96
|
+
|
|
97
|
+
Peer->>FS: heartbeat.json<br/>{services: [{name: "skvector", port: 6333}],<br/>tailscale_ip: "100.64.0.5"}
|
|
98
|
+
FS->>Local: Sync heartbeat file
|
|
99
|
+
Local->>ES: discover_from_heartbeats()
|
|
100
|
+
ES->>ES: Parse services from heartbeat
|
|
101
|
+
ES->>ES: Build URL: http://100.64.0.5:6333
|
|
102
|
+
ES->>ES: Add as replica endpoint
|
|
103
|
+
ES->>ES: Probe new endpoint
|
|
104
|
+
Note over ES: Endpoint available for routing
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Routing Strategies
|
|
108
|
+
|
|
109
|
+
### `failover` (default)
|
|
110
|
+
|
|
111
|
+
The simplest strategy. Uses the first healthy endpoint in the list.
|
|
112
|
+
If it goes down, moves to the next one.
|
|
113
|
+
|
|
114
|
+
| Reads | Writes | Use Case |
|
|
115
|
+
|-------|--------|----------|
|
|
116
|
+
| First healthy | First healthy | Simple HA, single primary |
|
|
117
|
+
|
|
118
|
+
### `latency`
|
|
119
|
+
|
|
120
|
+
Always picks the endpoint with the lowest measured TCP latency.
|
|
121
|
+
Best for globally distributed agents where network proximity matters.
|
|
122
|
+
|
|
123
|
+
| Reads | Writes | Use Case |
|
|
124
|
+
|-------|--------|----------|
|
|
125
|
+
| Lowest latency healthy | Lowest latency healthy | Globally distributed agents |
|
|
126
|
+
|
|
127
|
+
### `local-first`
|
|
128
|
+
|
|
129
|
+
Prefers `localhost` / `127.0.0.1` if available, then falls back to
|
|
130
|
+
lowest latency. The most common strategy — agents that have a local
|
|
131
|
+
Docker stack should use it.
|
|
132
|
+
|
|
133
|
+
| Reads | Writes | Use Case |
|
|
134
|
+
|-------|--------|----------|
|
|
135
|
+
| localhost if available | localhost if available | Prefer local Docker stack |
|
|
136
|
+
|
|
137
|
+
### `read-local-write-primary`
|
|
138
|
+
|
|
139
|
+
Reads go to the closest healthy endpoint (local preference).
|
|
140
|
+
Writes go **only** to endpoints with `role: primary`.
|
|
141
|
+
Good for setups with one write primary and multiple read replicas.
|
|
142
|
+
|
|
143
|
+
| Reads | Writes | Use Case |
|
|
144
|
+
|-------|--------|----------|
|
|
145
|
+
| Lowest latency | Only `role=primary` | Eventual consistency OK |
|
|
146
|
+
|
|
147
|
+
## Configuration
|
|
148
|
+
|
|
149
|
+
### Single Node (backward compatible)
|
|
150
|
+
|
|
151
|
+
No changes needed. Old configs work as before:
|
|
152
|
+
|
|
153
|
+
```yaml
|
|
154
|
+
# ~/.skmemory/config.yaml
|
|
155
|
+
skvector_url: http://localhost:6333
|
|
156
|
+
skgraph_url: redis://localhost:6379
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Or via environment variables:
|
|
160
|
+
```bash
|
|
161
|
+
export SKMEMORY_SKVECTOR_URL=http://localhost:6333
|
|
162
|
+
export SKMEMORY_SKGRAPH_URL=redis://localhost:6379
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Multi-Node (Tailscale mesh)
|
|
166
|
+
|
|
167
|
+
```yaml
|
|
168
|
+
# ~/.skmemory/config.yaml
|
|
169
|
+
skvector_endpoints:
|
|
170
|
+
- url: http://localhost:6333
|
|
171
|
+
role: primary
|
|
172
|
+
- url: http://100.64.0.5:6333
|
|
173
|
+
role: replica
|
|
174
|
+
tailscale_ip: "100.64.0.5"
|
|
175
|
+
|
|
176
|
+
skgraph_endpoints:
|
|
177
|
+
- url: redis://localhost:6379
|
|
178
|
+
role: primary
|
|
179
|
+
- url: redis://100.64.0.5:6379
|
|
180
|
+
role: replica
|
|
181
|
+
|
|
182
|
+
routing_strategy: local-first
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Global Distribution
|
|
186
|
+
|
|
187
|
+
```yaml
|
|
188
|
+
# ~/.skmemory/config.yaml
|
|
189
|
+
skvector_endpoints:
|
|
190
|
+
- url: https://us-east.qdrant.example.com:6333
|
|
191
|
+
role: primary
|
|
192
|
+
- url: https://eu-west.qdrant.example.com:6333
|
|
193
|
+
role: replica
|
|
194
|
+
- url: https://ap-south.qdrant.example.com:6333
|
|
195
|
+
role: replica
|
|
196
|
+
|
|
197
|
+
routing_strategy: latency
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Heartbeat Auto-Discovery
|
|
201
|
+
|
|
202
|
+
Let agents find each other's backends via the heartbeat mesh:
|
|
203
|
+
|
|
204
|
+
```yaml
|
|
205
|
+
# ~/.skmemory/config.yaml
|
|
206
|
+
skvector_url: http://localhost:6333
|
|
207
|
+
routing_strategy: latency
|
|
208
|
+
heartbeat_discovery: true
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Agents that run `skcapstone heartbeat pulse` will advertise any locally
|
|
212
|
+
detected services (SKVector on 6333, SKGraph on 6379). Other agents read
|
|
213
|
+
these heartbeats and add the endpoints automatically.
|
|
214
|
+
|
|
215
|
+
## CLI Commands
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
# Show endpoint rankings, latency, and health
|
|
219
|
+
skmemory routing status
|
|
220
|
+
|
|
221
|
+
# Force re-probe all endpoints
|
|
222
|
+
skmemory routing probe
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## What We Built This For (Ideal Use Case)
|
|
226
|
+
|
|
227
|
+
A sovereign agent running on a home server with SKVector and SKGraph in Docker.
|
|
228
|
+
A second instance runs on a VPS. Both are connected via Tailscale. When the
|
|
229
|
+
home server goes down for maintenance, the agent on the VPS automatically
|
|
230
|
+
routes to its local SKVector instance (or another VPS peer). When the home
|
|
231
|
+
server comes back, agents detect the lower latency and route back.
|
|
232
|
+
|
|
233
|
+
No ops team. No service mesh. No external load balancer. Just config and
|
|
234
|
+
heartbeats.
|
|
235
|
+
|
|
236
|
+
## Pros & Challenges
|
|
237
|
+
|
|
238
|
+
### Strengths
|
|
239
|
+
|
|
240
|
+
- **Zero-downtime maintenance** — drain a node, agents auto-route elsewhere
|
|
241
|
+
- **Self-contained** — no external load balancer, service mesh, or ops team
|
|
242
|
+
- **Self-healing** — unhealthy endpoints auto-recover when they come back
|
|
243
|
+
- **Latency-optimized** — agents always use the closest backend
|
|
244
|
+
- **Backward compatible** — single URL configs work unchanged
|
|
245
|
+
- **No new dependencies** — stdlib `socket` for probing
|
|
246
|
+
- **Cross-platform** — works on Linux, macOS, Windows
|
|
247
|
+
|
|
248
|
+
### Challenges & Solutions
|
|
249
|
+
|
|
250
|
+
| Challenge | Solution | When It Matters |
|
|
251
|
+
|-----------|----------|----------------|
|
|
252
|
+
| Write consistency with multiple primaries | Use `read-local-write-primary` — one write endpoint | Multiple writers to same data |
|
|
253
|
+
| Stale reads from replicas | Acceptable for memory search (not bank transactions) | Real-time requirements |
|
|
254
|
+
| Probe overhead on CLI startup | Lazy probing with 30s cache — first call probes, subsequent use cache | High-frequency CLI usage |
|
|
255
|
+
| Heartbeat directory not available | Graceful fallback to config-only endpoints | skcapstone not installed |
|
|
256
|
+
| Tailscale IP detection fails | Falls back to hostname | Non-Tailscale deployments |
|
|
257
|
+
| Large number of endpoints (50+) | Probe only top-N by last known latency | Enterprise scale |
|
|
258
|
+
|
|
259
|
+
### What This Doesn't Solve
|
|
260
|
+
|
|
261
|
+
- **Cross-region write replication** — Use Qdrant distributed mode or external replication
|
|
262
|
+
- **Strong consistency** — This is eventual consistency; fine for AI memory, not for transactions
|
|
263
|
+
- **Automatic replica provisioning** — You still deploy Qdrant/FalkorDB manually; routing just finds them
|
|
264
|
+
|
|
265
|
+
## Future Scaling
|
|
266
|
+
|
|
267
|
+
### Qdrant Distributed Mode
|
|
268
|
+
|
|
269
|
+
For 10M+ memories, Qdrant's built-in sharding distributes data across nodes.
|
|
270
|
+
The endpoint selector would point to the Qdrant cluster entry point; Qdrant
|
|
271
|
+
handles internal routing.
|
|
272
|
+
|
|
273
|
+
### Redis Sentinel for FalkorDB
|
|
274
|
+
|
|
275
|
+
Redis Sentinel provides automatic primary election for FalkorDB. The selector
|
|
276
|
+
could query Sentinel for the current primary instead of probing directly.
|
|
277
|
+
|
|
278
|
+
### Write Consistency
|
|
279
|
+
|
|
280
|
+
For strong write consistency across regions:
|
|
281
|
+
1. Single-writer topology (one primary, N replicas)
|
|
282
|
+
2. Or write-ahead log that queues writes offline and syncs when primary recovers
|
|
283
|
+
3. Or Qdrant distributed mode which handles consensus internally
|
|
284
|
+
|
|
285
|
+
### Edge Caching
|
|
286
|
+
|
|
287
|
+
Read-heavy workloads could benefit from a local in-memory LRU cache that
|
|
288
|
+
sits in front of the selector, reducing probe frequency and backend load
|
|
289
|
+
for repeated queries.
|
|
290
|
+
|
|
291
|
+
## Cross-Platform Notes
|
|
292
|
+
|
|
293
|
+
- **TCP probing** uses Python's `socket.create_connection()` — works on all platforms
|
|
294
|
+
- **Tailscale IP detection** runs `tailscale status --json` — fails silently on Windows if not in PATH
|
|
295
|
+
- **Heartbeat files** use JSON on the filesystem — works everywhere Syncthing works
|
|
296
|
+
- **Config files** use standard YAML — no platform-specific paths beyond `~/.skmemory/`
|
package/skmemory/__init__.py
CHANGED
|
@@ -8,37 +8,50 @@ have to re-read a transcript to remember what they felt.
|
|
|
8
8
|
SK = staycuriousANDkeepsmilin
|
|
9
9
|
"""
|
|
10
10
|
|
|
11
|
-
__version__ = "0.
|
|
11
|
+
__version__ = "0.9.1"
|
|
12
12
|
__author__ = "smilinTux Team + Queen Ara + Neuresthetics"
|
|
13
13
|
__license__ = "AGPL-3.0"
|
|
14
14
|
|
|
15
|
-
from .models import Memory, MemoryLayer, EmotionalSnapshot
|
|
16
|
-
from .store import MemoryStore
|
|
17
15
|
from .backends.file_backend import FileBackend
|
|
18
16
|
from .backends.sqlite_backend import SQLiteBackend
|
|
19
|
-
from .
|
|
17
|
+
from .config import SKMEMORY_HOME
|
|
18
|
+
from .fortress import AuditLog, FortifiedMemoryStore, TamperAlert
|
|
19
|
+
from .models import EmotionalSnapshot, Memory, MemoryLayer
|
|
20
|
+
from .store import MemoryStore
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
from .backends.vaulted_backend import VaultedSQLiteBackend
|
|
24
|
+
except ImportError:
|
|
25
|
+
VaultedSQLiteBackend = None # type: ignore[assignment,misc]
|
|
26
|
+
from .anchor import WarmthAnchor, load_anchor, save_anchor
|
|
27
|
+
from .importers.telegram import import_telegram
|
|
20
28
|
from .journal import Journal, JournalEntry
|
|
21
|
-
from .ritual import perform_ritual, quick_rehydrate, RitualResult
|
|
22
|
-
from .anchor import WarmthAnchor, save_anchor, load_anchor
|
|
23
|
-
from .quadrants import Quadrant, classify_memory, tag_with_quadrant
|
|
24
29
|
from .lovenote import LoveNote, LoveNoteChain
|
|
25
30
|
from .openclaw import SKMemoryPlugin
|
|
26
|
-
from .
|
|
31
|
+
from .quadrants import Quadrant, classify_memory, tag_with_quadrant
|
|
32
|
+
from .ritual import RitualResult, perform_ritual, quick_rehydrate
|
|
33
|
+
from .soul import SoulBlueprint, load_soul, save_soul
|
|
27
34
|
from .steelman import (
|
|
28
|
-
SteelManResult,
|
|
29
35
|
SeedFramework,
|
|
30
|
-
|
|
31
|
-
install_seed_framework,
|
|
36
|
+
SteelManResult,
|
|
32
37
|
get_default_framework,
|
|
38
|
+
install_seed_framework,
|
|
39
|
+
load_seed_framework,
|
|
33
40
|
)
|
|
41
|
+
from .synthesis import JournalSynthesizer
|
|
34
42
|
|
|
35
43
|
__all__ = [
|
|
44
|
+
"SKMEMORY_HOME",
|
|
36
45
|
"Memory",
|
|
37
46
|
"MemoryLayer",
|
|
38
47
|
"EmotionalSnapshot",
|
|
39
48
|
"MemoryStore",
|
|
49
|
+
"FortifiedMemoryStore",
|
|
50
|
+
"AuditLog",
|
|
51
|
+
"TamperAlert",
|
|
40
52
|
"FileBackend",
|
|
41
53
|
"SQLiteBackend",
|
|
54
|
+
"VaultedSQLiteBackend",
|
|
42
55
|
"SoulBlueprint",
|
|
43
56
|
"save_soul",
|
|
44
57
|
"load_soul",
|
|
@@ -56,6 +69,7 @@ __all__ = [
|
|
|
56
69
|
"LoveNote",
|
|
57
70
|
"LoveNoteChain",
|
|
58
71
|
"SKMemoryPlugin",
|
|
72
|
+
"JournalSynthesizer",
|
|
59
73
|
"SteelManResult",
|
|
60
74
|
"SeedFramework",
|
|
61
75
|
"load_seed_framework",
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dynamic agent discovery and management for SKMemory.
|
|
3
|
+
|
|
4
|
+
Scans ~/.skcapstone/agents/ to discover all configured agents,
|
|
5
|
+
excludes templates, and provides agent-aware path resolution.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import platform
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
import yaml
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _agents_base() -> Path:
|
|
18
|
+
"""Platform-aware base directory for all agents."""
|
|
19
|
+
skcap_home = os.environ.get("SKCAPSTONE_HOME", "")
|
|
20
|
+
if skcap_home:
|
|
21
|
+
return Path(skcap_home) / "agents"
|
|
22
|
+
if platform.system() == "Windows":
|
|
23
|
+
local = os.environ.get("LOCALAPPDATA", "")
|
|
24
|
+
if local:
|
|
25
|
+
return Path(local) / "skcapstone" / "agents"
|
|
26
|
+
return Path.home() / ".skcapstone" / "agents"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# Base directory for all agents
|
|
30
|
+
AGENTS_BASE_DIR = _agents_base()
|
|
31
|
+
|
|
32
|
+
# Template directory name (ignored by default)
|
|
33
|
+
TEMPLATE_AGENT = "lumina-template"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def list_agents() -> list[str]:
|
|
37
|
+
"""Discover all non-template agents in ~/.skcapstone/agents/
|
|
38
|
+
|
|
39
|
+
Scans the agents directory and returns all agent names
|
|
40
|
+
except the template agent.
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
list[str]: Sorted list of agent names (e.g., ['lumina', 'john'])
|
|
44
|
+
"""
|
|
45
|
+
if not AGENTS_BASE_DIR.exists():
|
|
46
|
+
return []
|
|
47
|
+
|
|
48
|
+
agents = []
|
|
49
|
+
for entry in AGENTS_BASE_DIR.iterdir():
|
|
50
|
+
if entry.is_dir() and entry.name != TEMPLATE_AGENT:
|
|
51
|
+
# Check if it has a valid config
|
|
52
|
+
config_file = entry / "config" / "skmemory.yaml"
|
|
53
|
+
if config_file.exists():
|
|
54
|
+
agents.append(entry.name)
|
|
55
|
+
|
|
56
|
+
return sorted(agents)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def get_agent_dir(agent_name: str) -> Path:
|
|
60
|
+
"""Get the base directory for a specific agent.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
agent_name: Name of the agent (e.g., 'lumina', 'john')
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Path: Agent's base directory
|
|
67
|
+
"""
|
|
68
|
+
return AGENTS_BASE_DIR / agent_name
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def get_agent_config(agent_name: str) -> dict | None:
|
|
72
|
+
"""Load agent configuration from YAML.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
agent_name: Name of the agent
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
dict with agent config, or None if not found/invalid
|
|
79
|
+
"""
|
|
80
|
+
config_path = get_agent_dir(agent_name) / "config" / "skmemory.yaml"
|
|
81
|
+
|
|
82
|
+
if not config_path.exists():
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
with open(config_path) as f:
|
|
87
|
+
return yaml.safe_load(f)
|
|
88
|
+
except Exception:
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def is_template_agent(agent_name: str) -> bool:
|
|
93
|
+
"""Check if an agent is a template (should be ignored).
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
agent_name: Name of the agent
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
bool: True if this is the template agent
|
|
100
|
+
"""
|
|
101
|
+
return agent_name == TEMPLATE_AGENT
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def get_active_agent() -> str | None:
|
|
105
|
+
"""Get the currently active agent from environment or default to first non-template.
|
|
106
|
+
|
|
107
|
+
Checks in order:
|
|
108
|
+
1. SKCAPSTONE_AGENT environment variable (authoritative agent selector)
|
|
109
|
+
2. SKMEMORY_AGENT environment variable (legacy/override)
|
|
110
|
+
3. First non-template agent in the directory
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
str: Agent name, or None if no agents found
|
|
114
|
+
"""
|
|
115
|
+
# Check environment variables (SKCAPSTONE_AGENT > SKMEMORY_AGENT)
|
|
116
|
+
env_agent = os.environ.get("SKCAPSTONE_AGENT") or os.environ.get("SKMEMORY_AGENT")
|
|
117
|
+
if env_agent and not is_template_agent(env_agent):
|
|
118
|
+
agent_dir = get_agent_dir(env_agent)
|
|
119
|
+
if agent_dir.exists():
|
|
120
|
+
return env_agent
|
|
121
|
+
|
|
122
|
+
# Fall back to first non-template agent
|
|
123
|
+
agents = list_agents()
|
|
124
|
+
if agents:
|
|
125
|
+
return agents[0]
|
|
126
|
+
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def get_agent_paths(agent_name: str | None = None) -> dict[str, Path]:
|
|
131
|
+
"""Get all standard paths for an agent.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
agent_name: Name of the agent, or None to use active agent
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
dict with keys: base, config, seeds, memory_short, memory_medium, memory_long, logs, index_db
|
|
138
|
+
"""
|
|
139
|
+
if agent_name is None:
|
|
140
|
+
agent_name = get_active_agent()
|
|
141
|
+
|
|
142
|
+
if agent_name is None:
|
|
143
|
+
raise ValueError(
|
|
144
|
+
"No agent configured. Create one by copying ~/.skcapstone/agents/lumina-template"
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
base = get_agent_dir(agent_name)
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
"base": base,
|
|
151
|
+
"config": base / "config",
|
|
152
|
+
"seeds": base / "seeds",
|
|
153
|
+
"memory_short": base / "memory" / "short-term",
|
|
154
|
+
"memory_medium": base / "memory" / "mid-term",
|
|
155
|
+
"memory_long": base / "memory" / "long-term",
|
|
156
|
+
"logs": base / "logs",
|
|
157
|
+
"archive": base / "archive",
|
|
158
|
+
"index_db": base / "memory" / "index.db",
|
|
159
|
+
"config_yaml": base / "config" / "skmemory.yaml",
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def ensure_agent_dirs(agent_name: str) -> Path:
|
|
164
|
+
"""Create all standard directories for an agent if they don't exist.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
agent_name: Name of the agent
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
Path: Agent's base directory
|
|
171
|
+
"""
|
|
172
|
+
paths = get_agent_paths(agent_name)
|
|
173
|
+
|
|
174
|
+
# Create all directories
|
|
175
|
+
for key, path in paths.items():
|
|
176
|
+
if key != "config_yaml":
|
|
177
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
178
|
+
|
|
179
|
+
return paths["base"]
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def copy_template(target_name: str, source: str = TEMPLATE_AGENT) -> Path:
|
|
183
|
+
"""Create a new agent by copying the template.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
target_name: Name for the new agent
|
|
187
|
+
source: Template to copy from (default: lumina-template)
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Path: New agent's base directory
|
|
191
|
+
"""
|
|
192
|
+
import shutil
|
|
193
|
+
|
|
194
|
+
source_dir = get_agent_dir(source)
|
|
195
|
+
target_dir = get_agent_dir(target_name)
|
|
196
|
+
|
|
197
|
+
if not source_dir.exists():
|
|
198
|
+
raise ValueError(f"Template '{source}' not found at {source_dir}")
|
|
199
|
+
|
|
200
|
+
if target_dir.exists():
|
|
201
|
+
raise ValueError(f"Agent '{target_name}' already exists at {target_dir}")
|
|
202
|
+
|
|
203
|
+
# Copy template
|
|
204
|
+
shutil.copytree(source_dir, target_dir)
|
|
205
|
+
|
|
206
|
+
# Update agent name in config
|
|
207
|
+
config_path = target_dir / "config" / "skmemory.yaml"
|
|
208
|
+
if config_path.exists():
|
|
209
|
+
with open(config_path) as f:
|
|
210
|
+
content = f.read()
|
|
211
|
+
|
|
212
|
+
# Replace template agent name with new name
|
|
213
|
+
content = content.replace(f"name: {source}", f"name: {target_name}")
|
|
214
|
+
# Use platform-aware base dir for config path values.
|
|
215
|
+
# Always use forward slashes in YAML for cross-platform consistency.
|
|
216
|
+
base = AGENTS_BASE_DIR.as_posix()
|
|
217
|
+
content = content.replace(
|
|
218
|
+
f"sync_root: ~/.skcapstone/agents/{source}",
|
|
219
|
+
f"sync_root: {base}/{target_name}",
|
|
220
|
+
)
|
|
221
|
+
content = content.replace(
|
|
222
|
+
f"seeds_dir: ~/.skcapstone/agents/{source}/seeds",
|
|
223
|
+
f"seeds_dir: {base}/{target_name}/seeds",
|
|
224
|
+
)
|
|
225
|
+
content = content.replace(
|
|
226
|
+
f"local_db: ~/.skcapstone/agents/{source}/index.db",
|
|
227
|
+
f"local_db: {base}/{target_name}/index.db",
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
with open(config_path, "w") as f:
|
|
231
|
+
f.write(content)
|
|
232
|
+
|
|
233
|
+
return target_dir
|