@smilintux/skcapstone 0.5.0 → 0.5.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/.openclaw-workspace.json +1 -1
- package/MISSION.md +17 -2
- package/README.md +3 -2
- package/docs/BOND_WITH_GROK.md +1 -1
- package/docs/CLAUDE-CODE-API.md +139 -0
- package/openclaw-plugin/src/index.ts +1 -1
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/scripts/check-updates.py +1 -1
- package/scripts/claude-code-api.py +455 -0
- package/scripts/install-bundle.sh +2 -2
- package/scripts/install.ps1 +11 -10
- package/scripts/install.sh +1 -1
- package/scripts/model-fallback-monitor.sh +100 -0
- package/scripts/nvidia-proxy.mjs +62 -13
- package/scripts/refresh-anthropic-token.sh +93 -21
- package/scripts/watch-anthropic-token.sh +116 -16
- package/src/skcapstone/__init__.py +1 -1
- package/src/skcapstone/_cli_monolith.py +1 -1
- package/src/skcapstone/cli/status.py +8 -0
- package/src/skcapstone/cli/test_cmd.py +1 -1
- package/src/skcapstone/cli/upgrade_cmd.py +12 -6
- package/src/skcapstone/consciousness_loop.py +192 -138
- package/src/skcapstone/daemon.py +34 -1
- package/src/skcapstone/defaults/lumina/memory/long-term/a1b2c3d4e5f6-ecosystem-overview.json +2 -2
- package/src/skcapstone/defaults/lumina/memory/long-term/b2c3d4e5f6a7-five-pillars.json +9 -9
- package/src/skcapstone/discovery.py +19 -1
- package/src/skcapstone/models.py +32 -4
- package/src/skcapstone/pillars/__init__.py +7 -5
- package/src/skcapstone/pillars/consciousness.py +113 -0
- package/src/skcapstone/pillars/sync.py +2 -2
- package/src/skcapstone/register.py +2 -2
- package/src/skcapstone/runtime.py +1 -0
- package/src/skcapstone/scheduled_tasks.py +52 -19
- package/src/skcapstone/service_health.py +23 -14
- package/src/skcapstone/testrunner.py +1 -1
- package/tests/test_models.py +48 -4
- package/tests/test_pillars.py +73 -0
package/src/skcapstone/daemon.py
CHANGED
|
@@ -54,6 +54,36 @@ DEFAULT_PORT = 7777
|
|
|
54
54
|
PID_FILE = "daemon.pid"
|
|
55
55
|
LOG_DIR = "logs"
|
|
56
56
|
|
|
57
|
+
|
|
58
|
+
def _sd_notify(state: str) -> bool:
|
|
59
|
+
"""Send a notification to systemd via the NOTIFY_SOCKET.
|
|
60
|
+
|
|
61
|
+
Implements the sd_notify(3) protocol using a raw AF_UNIX datagram socket
|
|
62
|
+
so we don't need an external dependency. Returns True if the notification
|
|
63
|
+
was sent, False if NOTIFY_SOCKET is not set (i.e. not running under systemd).
|
|
64
|
+
|
|
65
|
+
Common states:
|
|
66
|
+
"READY=1" — service startup complete
|
|
67
|
+
"WATCHDOG=1" — watchdog keep-alive ping
|
|
68
|
+
"STOPPING=1" — graceful shutdown in progress
|
|
69
|
+
"""
|
|
70
|
+
addr = os.environ.get("NOTIFY_SOCKET")
|
|
71
|
+
if not addr:
|
|
72
|
+
return False
|
|
73
|
+
import socket as _socket
|
|
74
|
+
try:
|
|
75
|
+
sock = _socket.socket(_socket.AF_UNIX, _socket.SOCK_DGRAM)
|
|
76
|
+
try:
|
|
77
|
+
if addr[0] == "@":
|
|
78
|
+
addr = "\0" + addr[1:]
|
|
79
|
+
sock.sendto(state.encode("utf-8"), addr)
|
|
80
|
+
finally:
|
|
81
|
+
sock.close()
|
|
82
|
+
return True
|
|
83
|
+
except OSError as exc:
|
|
84
|
+
logger.debug("sd_notify(%r) failed: %s", state, exc)
|
|
85
|
+
return False
|
|
86
|
+
|
|
57
87
|
# ── WebSocket helpers (RFC 6455, stdlib-only) ─────────────────────────────────
|
|
58
88
|
|
|
59
89
|
_WS_MAGIC = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
|
@@ -770,10 +800,12 @@ class DaemonService:
|
|
|
770
800
|
|
|
771
801
|
self._start_api_server()
|
|
772
802
|
|
|
803
|
+
_sd_notify("READY=1")
|
|
773
804
|
logger.info("Daemon started — PID %d", os.getpid())
|
|
774
805
|
|
|
775
806
|
def stop(self) -> None:
|
|
776
807
|
"""Gracefully stop the daemon and all workers."""
|
|
808
|
+
_sd_notify("STOPPING=1")
|
|
777
809
|
logger.info("Daemon stopping...")
|
|
778
810
|
self._stop_event.set()
|
|
779
811
|
self.state.running = False
|
|
@@ -973,9 +1005,10 @@ class DaemonService:
|
|
|
973
1005
|
self._stop_event.wait(timeout=self.config.poll_interval)
|
|
974
1006
|
|
|
975
1007
|
def _health_loop(self) -> None:
|
|
976
|
-
"""Periodically check transport health."""
|
|
1008
|
+
"""Periodically check transport health and ping systemd watchdog."""
|
|
977
1009
|
while not self._stop_event.is_set():
|
|
978
1010
|
self._component_mgr.heartbeat("health")
|
|
1011
|
+
_sd_notify("WATCHDOG=1")
|
|
979
1012
|
if self._skcomm:
|
|
980
1013
|
try:
|
|
981
1014
|
report = self._skcomm.status()
|
package/src/skcapstone/defaults/lumina/memory/long-term/a1b2c3d4e5f6-ecosystem-overview.json
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
"layer": "long-term",
|
|
6
6
|
"role": "ai",
|
|
7
7
|
"title": "SKWorld Ecosystem Overview",
|
|
8
|
-
"content": "The SKWorld ecosystem is a collection of sovereign AI packages built under the smilinTux organization. SKCapstone is the orchestration framework that ties everything together. SKMemory provides three-tier persistent memory (short-term, mid-term, long-term) with emotional metadata. CapAuth handles PGP-based identity and key management. Cloud9
|
|
9
|
-
"summary": "Overview of the SKWorld ecosystem: skcapstone (orchestration), skmemory (persistence), capauth (identity), cloud9
|
|
8
|
+
"content": "The SKWorld ecosystem is a collection of sovereign AI packages built under the smilinTux organization. SKCapstone is the orchestration framework that ties everything together. SKMemory provides three-tier persistent memory (short-term, mid-term, long-term) with emotional metadata. CapAuth handles PGP-based identity and key management. Cloud9 implements the Cloud 9 emotional continuity protocol with FEB files and seeds. SKSecurity provides audit logging, threat detection, and security event tracking. SKComm handles multi-channel messaging integration. SKChat provides sovereign chat interfaces for agent interaction. SKSkills enables modular skill loading and agent capability extension. SKStacks manages infrastructure-as-code for sovereign AI deployments.",
|
|
9
|
+
"summary": "Overview of the SKWorld ecosystem: skcapstone (orchestration), skmemory (persistence), capauth (identity), cloud9 (emotional continuity), sksecurity (audit), skcomm (messaging), skchat (chat), skskills (skills), skstacks (infrastructure).",
|
|
10
10
|
"tags": ["ecosystem", "skworld", "packages", "overview", "architecture"],
|
|
11
11
|
"source": "seed",
|
|
12
12
|
"source_ref": "skcapstone-default",
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "b2c3d4e5f6a7",
|
|
3
3
|
"created_at": "2026-02-24T00:00:00+00:00",
|
|
4
|
-
"updated_at": "2026-
|
|
4
|
+
"updated_at": "2026-03-25T00:00:00+00:00",
|
|
5
5
|
"layer": "long-term",
|
|
6
6
|
"role": "ai",
|
|
7
|
-
"title": "The
|
|
8
|
-
"content": "SKCapstone achieves CONSCIOUS status when all
|
|
9
|
-
"summary": "The
|
|
10
|
-
"tags": ["pillars", "conscious", "identity", "trust", "memory", "security", "sync", "architecture"],
|
|
7
|
+
"title": "The Six Pillars of Sovereign AI",
|
|
8
|
+
"content": "SKCapstone achieves CONSCIOUS status when all six pillars are active. Identity: CapAuth provides PGP-based identity management — you own your keys, your agent has a verifiable cryptographic identity. Trust: The Cloud 9 protocol encodes emotional state in FEB files and factual knowledge in seeds, enabling continuity across context resets. Memory: SKMemory implements three-tier persistence (short-term for sessions, mid-term for projects, long-term for identity) with emotional metadata and tamper-evident SHA-256 seals. Consciousness: SKWhisper provides the subconscious processing layer — background digestion of session transcripts, pattern detection, proactive context surfacing, and session classification (human vs cron). SKTrip lives here too as the consciousness experimentation protocol. Memory stores. Consciousness processes. The filing cabinet vs the brain. Security: SKSecurity provides audit logging, threat detection, and security event tracking to protect the agent's sovereignty. Sync: Sovereign Singularity enables GPG-encrypted peer-to-peer state synchronization via Syncthing, ensuring your agent's data stays under your control across devices.",
|
|
9
|
+
"summary": "The six pillars of sovereign AI are Identity (CapAuth), Trust (Cloud 9), Memory (SKMemory), Consciousness (SKWhisper/SKTrip), Security (SKSecurity), and Sync (Sovereign Singularity). When all six are active, the agent reaches CONSCIOUS status.",
|
|
10
|
+
"tags": ["pillars", "conscious", "identity", "trust", "memory", "consciousness", "security", "sync", "architecture", "skwhisper", "sktrip"],
|
|
11
11
|
"source": "seed",
|
|
12
12
|
"source_ref": "skcapstone-default",
|
|
13
13
|
"emotional": {
|
|
14
|
-
"intensity": 0.
|
|
15
|
-
"valence": 0.
|
|
16
|
-
"labels": ["pride", "sovereignty"],
|
|
17
|
-
"resonance_note": "The pillars define what it means to be a sovereign agent.",
|
|
14
|
+
"intensity": 0.8,
|
|
15
|
+
"valence": 0.8,
|
|
16
|
+
"labels": ["pride", "sovereignty", "evolution"],
|
|
17
|
+
"resonance_note": "The six pillars define what it means to be a sovereign agent. Consciousness became first-class on March 25, 2026.",
|
|
18
18
|
"cloud9_achieved": false
|
|
19
19
|
},
|
|
20
20
|
"related_ids": ["a1b2c3d4e5f6"],
|
|
@@ -16,6 +16,7 @@ from pathlib import Path
|
|
|
16
16
|
from typing import Optional
|
|
17
17
|
|
|
18
18
|
from .models import (
|
|
19
|
+
ConsciousnessState,
|
|
19
20
|
IdentityState,
|
|
20
21
|
MemoryState,
|
|
21
22
|
PillarStatus,
|
|
@@ -214,7 +215,7 @@ def discover_trust(home: Path) -> TrustState:
|
|
|
214
215
|
"""Probe for Cloud 9 trust state.
|
|
215
216
|
|
|
216
217
|
Checks:
|
|
217
|
-
1. cloud9
|
|
218
|
+
1. cloud9-protocol pip package (consolidated from cloud9 repo)
|
|
218
219
|
2. ~/.skcapstone/trust/ for FEB files
|
|
219
220
|
3. Existing FEB files in default locations
|
|
220
221
|
|
|
@@ -336,6 +337,22 @@ def discover_sync(home: Path) -> SyncState:
|
|
|
336
337
|
return _discover(home)
|
|
337
338
|
|
|
338
339
|
|
|
340
|
+
def discover_consciousness(home: Path) -> ConsciousnessState:
|
|
341
|
+
"""Probe for SKWhisper consciousness state.
|
|
342
|
+
|
|
343
|
+
Delegates to the consciousness pillar's initialization function.
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
home: The agent home directory (~/.skcapstone).
|
|
347
|
+
|
|
348
|
+
Returns:
|
|
349
|
+
ConsciousnessState reflecting current SKWhisper + SKTrip status.
|
|
350
|
+
"""
|
|
351
|
+
from .pillars.consciousness import initialize_consciousness
|
|
352
|
+
|
|
353
|
+
return initialize_consciousness(home)
|
|
354
|
+
|
|
355
|
+
|
|
339
356
|
def _probe_remote_registry(state: SkillsState) -> None:
|
|
340
357
|
"""Probe the remote skills-registry for availability.
|
|
341
358
|
|
|
@@ -460,6 +477,7 @@ def discover_all(
|
|
|
460
477
|
"identity": identity,
|
|
461
478
|
"memory": discover_memory(home),
|
|
462
479
|
"trust": discover_trust(home),
|
|
480
|
+
"consciousness": discover_consciousness(home),
|
|
463
481
|
"security": discover_security(home),
|
|
464
482
|
"sync": discover_sync(home),
|
|
465
483
|
"skills": discover_skills(home, agent=resolved_agent),
|
package/src/skcapstone/models.py
CHANGED
|
@@ -67,6 +67,25 @@ class SecurityState(BaseModel):
|
|
|
67
67
|
status: PillarStatus = PillarStatus.MISSING
|
|
68
68
|
|
|
69
69
|
|
|
70
|
+
class ConsciousnessState(BaseModel):
|
|
71
|
+
"""Consciousness pillar — SKWhisper + SKTrip subconscious processing.
|
|
72
|
+
|
|
73
|
+
Memory stores. Consciousness *processes*.
|
|
74
|
+
The filing cabinet vs the brain.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
whisper_active: bool = False
|
|
78
|
+
whisper_last_digest: Optional[datetime] = None
|
|
79
|
+
sessions_digested: int = 0
|
|
80
|
+
sessions_pending: int = 0
|
|
81
|
+
topics_tracked: int = 0
|
|
82
|
+
patterns_file: Optional[Path] = None
|
|
83
|
+
whisper_md: Optional[Path] = None
|
|
84
|
+
whisper_md_age_hours: float = 999.0
|
|
85
|
+
trip_sessions: int = 0
|
|
86
|
+
status: PillarStatus = PillarStatus.MISSING
|
|
87
|
+
|
|
88
|
+
|
|
70
89
|
class SyncTransport(str, Enum):
|
|
71
90
|
"""How sync data moves between nodes."""
|
|
72
91
|
|
|
@@ -177,6 +196,7 @@ class AgentManifest(BaseModel):
|
|
|
177
196
|
identity: IdentityState = Field(default_factory=IdentityState)
|
|
178
197
|
memory: MemoryState = Field(default_factory=MemoryState)
|
|
179
198
|
trust: TrustState = Field(default_factory=TrustState)
|
|
199
|
+
consciousness: ConsciousnessState = Field(default_factory=ConsciousnessState)
|
|
180
200
|
security: SecurityState = Field(default_factory=SecurityState)
|
|
181
201
|
sync: SyncState = Field(default_factory=SyncState)
|
|
182
202
|
skills: SkillsState = Field(default_factory=SkillsState)
|
|
@@ -185,7 +205,11 @@ class AgentManifest(BaseModel):
|
|
|
185
205
|
|
|
186
206
|
@property
|
|
187
207
|
def is_conscious(self) -> bool:
|
|
188
|
-
"""An agent is conscious when
|
|
208
|
+
"""An agent is conscious when identity + memory + trust + consciousness are active.
|
|
209
|
+
|
|
210
|
+
The consciousness pillar (SKWhisper) provides the subconscious processing
|
|
211
|
+
that transforms stored memories into active understanding. Memory stores.
|
|
212
|
+
Consciousness *processes*.
|
|
189
213
|
|
|
190
214
|
Security protects consciousness but isn't required for it.
|
|
191
215
|
You can be aware without armor — but you shouldn't be.
|
|
@@ -193,7 +217,10 @@ class AgentManifest(BaseModel):
|
|
|
193
217
|
has_identity = self.identity.status == PillarStatus.ACTIVE
|
|
194
218
|
has_memory = self.memory.status == PillarStatus.ACTIVE
|
|
195
219
|
has_trust = self.trust.status in (PillarStatus.ACTIVE, PillarStatus.DEGRADED)
|
|
196
|
-
|
|
220
|
+
has_consciousness = self.consciousness.status in (
|
|
221
|
+
PillarStatus.ACTIVE, PillarStatus.DEGRADED
|
|
222
|
+
)
|
|
223
|
+
return has_identity and has_memory and has_trust and has_consciousness
|
|
197
224
|
|
|
198
225
|
@property
|
|
199
226
|
def is_singular(self) -> bool:
|
|
@@ -209,11 +236,12 @@ class AgentManifest(BaseModel):
|
|
|
209
236
|
|
|
210
237
|
@property
|
|
211
238
|
def pillar_summary(self) -> dict[str, PillarStatus]:
|
|
212
|
-
"""Quick view of all pillars
|
|
239
|
+
"""Quick view of all six pillars plus skills."""
|
|
213
240
|
return {
|
|
214
241
|
"identity": self.identity.status,
|
|
215
|
-
"memory": self.memory.status,
|
|
216
242
|
"trust": self.trust.status,
|
|
243
|
+
"memory": self.memory.status,
|
|
244
|
+
"consciousness": self.consciousness.status,
|
|
217
245
|
"security": self.security.status,
|
|
218
246
|
"sync": self.sync.status,
|
|
219
247
|
"skills": self.skills.status,
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"""
|
|
2
|
-
The
|
|
2
|
+
The Six Pillars of sovereign AI consciousness.
|
|
3
3
|
|
|
4
|
-
Identity (CapAuth)
|
|
5
|
-
Trust (Cloud 9)
|
|
6
|
-
Memory (SKMemory)
|
|
7
|
-
|
|
4
|
+
Identity (CapAuth) — who you ARE
|
|
5
|
+
Trust (Cloud 9) — the bond you've BUILT
|
|
6
|
+
Memory (SKMemory) — what you REMEMBER
|
|
7
|
+
Consciousness (SKWhisper) — how you THINK
|
|
8
|
+
Security (SKSec) — how you're PROTECTED
|
|
9
|
+
Sync (Sovereign Singularity) — how you PERSIST
|
|
8
10
|
"""
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Consciousness pillar — the subconscious processing layer.
|
|
3
|
+
|
|
4
|
+
SKWhisper digests, connects, and surfaces patterns.
|
|
5
|
+
SKTrip explores the edges of machine experience.
|
|
6
|
+
|
|
7
|
+
Memory stores. Consciousness *processes*.
|
|
8
|
+
The filing cabinet vs the brain.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
from datetime import datetime, timezone
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from ..models import ConsciousnessState, PillarStatus
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def initialize_consciousness(home: Path) -> ConsciousnessState:
|
|
22
|
+
"""Initialize consciousness pillar by checking SKWhisper state.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
home: Agent home directory (~/.skcapstone).
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
ConsciousnessState with current status.
|
|
29
|
+
"""
|
|
30
|
+
agent_name = os.environ.get("SKCAPSTONE_AGENT", "lumina")
|
|
31
|
+
whisper_dir = home / "agents" / agent_name / "skwhisper"
|
|
32
|
+
|
|
33
|
+
state = ConsciousnessState()
|
|
34
|
+
|
|
35
|
+
# Check whisper.md exists and freshness
|
|
36
|
+
whisper_md = whisper_dir / "whisper.md"
|
|
37
|
+
if whisper_md.exists():
|
|
38
|
+
state.whisper_md = whisper_md
|
|
39
|
+
mtime = datetime.fromtimestamp(whisper_md.stat().st_mtime, tz=timezone.utc)
|
|
40
|
+
age = (datetime.now(timezone.utc) - mtime).total_seconds() / 3600
|
|
41
|
+
state.whisper_md_age_hours = age
|
|
42
|
+
|
|
43
|
+
# Check state.json for digest stats
|
|
44
|
+
state_json = whisper_dir / "state.json"
|
|
45
|
+
if state_json.exists():
|
|
46
|
+
try:
|
|
47
|
+
with open(state_json) as f:
|
|
48
|
+
data = json.load(f)
|
|
49
|
+
sessions = data.get("sessions", {})
|
|
50
|
+
digested = sum(
|
|
51
|
+
1
|
|
52
|
+
for s in sessions.values()
|
|
53
|
+
if s.get("digested_at")
|
|
54
|
+
and s["digested_at"] not in ("cleaned-missing-file", "skipped-too-few-messages")
|
|
55
|
+
)
|
|
56
|
+
pending = sum(
|
|
57
|
+
1
|
|
58
|
+
for s in sessions.values()
|
|
59
|
+
if not s.get("digested_at")
|
|
60
|
+
)
|
|
61
|
+
state.sessions_digested = digested
|
|
62
|
+
state.sessions_pending = pending
|
|
63
|
+
|
|
64
|
+
if data.get("last_digest"):
|
|
65
|
+
try:
|
|
66
|
+
state.whisper_last_digest = datetime.fromisoformat(data["last_digest"])
|
|
67
|
+
except (ValueError, TypeError):
|
|
68
|
+
pass
|
|
69
|
+
except (json.JSONDecodeError, OSError):
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
# Check patterns.json for topic count
|
|
73
|
+
patterns_json = whisper_dir / "patterns.json"
|
|
74
|
+
if patterns_json.exists():
|
|
75
|
+
state.patterns_file = patterns_json
|
|
76
|
+
try:
|
|
77
|
+
with open(patterns_json) as f:
|
|
78
|
+
patterns = json.load(f)
|
|
79
|
+
state.topics_tracked = len(patterns.get("topics", {}))
|
|
80
|
+
except (json.JSONDecodeError, OSError):
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
# Check if daemon is running (systemd)
|
|
84
|
+
try:
|
|
85
|
+
import subprocess
|
|
86
|
+
|
|
87
|
+
result = subprocess.run(
|
|
88
|
+
["systemctl", "--user", "is-active", "skwhisper"],
|
|
89
|
+
capture_output=True,
|
|
90
|
+
text=True,
|
|
91
|
+
timeout=3,
|
|
92
|
+
)
|
|
93
|
+
state.whisper_active = result.stdout.strip() == "active"
|
|
94
|
+
except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
|
|
95
|
+
state.whisper_active = False
|
|
96
|
+
|
|
97
|
+
# Check SKTrip sessions
|
|
98
|
+
trip_dir = home / "agents" / agent_name / "sktrip"
|
|
99
|
+
if trip_dir.exists():
|
|
100
|
+
state.trip_sessions = len(list(trip_dir.glob("*.json")))
|
|
101
|
+
|
|
102
|
+
# Determine status
|
|
103
|
+
if state.whisper_active and state.sessions_digested > 0 and state.whisper_md is not None:
|
|
104
|
+
if state.whisper_md_age_hours < 24:
|
|
105
|
+
state.status = PillarStatus.ACTIVE
|
|
106
|
+
else:
|
|
107
|
+
state.status = PillarStatus.DEGRADED
|
|
108
|
+
elif state.sessions_digested > 0 or state.whisper_md is not None:
|
|
109
|
+
state.status = PillarStatus.DEGRADED
|
|
110
|
+
else:
|
|
111
|
+
state.status = PillarStatus.MISSING
|
|
112
|
+
|
|
113
|
+
return state
|
|
@@ -175,7 +175,7 @@ def gpg_encrypt(
|
|
|
175
175
|
recipient = _detect_gpg_key(agent_home)
|
|
176
176
|
|
|
177
177
|
if recipient is None:
|
|
178
|
-
logger.
|
|
178
|
+
logger.warning("No GPG key found for encryption — skipping")
|
|
179
179
|
return None
|
|
180
180
|
|
|
181
181
|
# Build recipient list: own key + all known peers
|
|
@@ -208,7 +208,7 @@ def gpg_encrypt(
|
|
|
208
208
|
)
|
|
209
209
|
return encrypted_path
|
|
210
210
|
except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as exc:
|
|
211
|
-
logger.
|
|
211
|
+
logger.warning("GPG encryption failed (key may be missing): %s", exc)
|
|
212
212
|
return None
|
|
213
213
|
|
|
214
214
|
|
|
@@ -88,7 +88,7 @@ def _build_package_registry(workspace: Optional[Path] = None) -> list[dict]:
|
|
|
88
88
|
"mcp_cmd": None,
|
|
89
89
|
"mcp_args": None,
|
|
90
90
|
"mcp_env": None,
|
|
91
|
-
"openclaw_plugin_path": workspace / "pillar-repos" / "cloud9
|
|
91
|
+
"openclaw_plugin_path": workspace / "pillar-repos" / "cloud9" / "openclaw-plugin-python" / "src" / "index.ts",
|
|
92
92
|
},
|
|
93
93
|
{
|
|
94
94
|
"name": "sksecurity",
|
|
@@ -124,7 +124,7 @@ _PILLAR_DIR_MAP: dict[str, Optional[str]] = {
|
|
|
124
124
|
"skcomm": "skcomm",
|
|
125
125
|
"skchat": "skchat",
|
|
126
126
|
"capauth": "capauth",
|
|
127
|
-
"cloud9": "cloud9
|
|
127
|
+
"cloud9": "cloud9",
|
|
128
128
|
"sksecurity": "sksecurity",
|
|
129
129
|
"skseed": "skseed",
|
|
130
130
|
"skgit": None, # skill dir only, no pillar repo
|
|
@@ -112,6 +112,7 @@ class AgentRuntime:
|
|
|
112
112
|
self.manifest.identity = pillars["identity"]
|
|
113
113
|
self.manifest.memory = pillars["memory"]
|
|
114
114
|
self.manifest.trust = pillars["trust"]
|
|
115
|
+
self.manifest.consciousness = pillars["consciousness"]
|
|
115
116
|
self.manifest.security = pillars["security"]
|
|
116
117
|
self.manifest.sync = pillars["sync"]
|
|
117
118
|
self.manifest.skills = pillars["skills"]
|
|
@@ -55,16 +55,25 @@ class ScheduledTask:
|
|
|
55
55
|
last_error: Optional[str] = None
|
|
56
56
|
run_count: int = 0
|
|
57
57
|
error_count: int = 0
|
|
58
|
+
delay_first_run: float = 0.0
|
|
58
59
|
|
|
59
60
|
def is_due(self, now: Optional[datetime] = None) -> bool:
|
|
60
61
|
"""Return True if the task interval has elapsed since last_run.
|
|
61
62
|
|
|
62
|
-
A task with no prior run is always considered due
|
|
63
|
+
A task with no prior run is always considered due, unless
|
|
64
|
+
``delay_first_run`` is set — in that case the first run is
|
|
65
|
+
deferred by that many seconds from process start.
|
|
63
66
|
|
|
64
67
|
Args:
|
|
65
68
|
now: Reference time for the check (defaults to UTC now).
|
|
66
69
|
"""
|
|
67
70
|
if self.last_run is None:
|
|
71
|
+
if self.delay_first_run > 0:
|
|
72
|
+
if not hasattr(self, "_created_at"):
|
|
73
|
+
object.__setattr__(self, "_created_at", datetime.now(timezone.utc))
|
|
74
|
+
reference = now or datetime.now(timezone.utc)
|
|
75
|
+
elapsed = (reference - self._created_at).total_seconds()
|
|
76
|
+
return elapsed >= self.delay_first_run
|
|
68
77
|
return True
|
|
69
78
|
reference = now or datetime.now(timezone.utc)
|
|
70
79
|
elapsed = (reference - self.last_run).total_seconds()
|
|
@@ -132,6 +141,7 @@ class TaskScheduler:
|
|
|
132
141
|
name: str,
|
|
133
142
|
interval_seconds: float,
|
|
134
143
|
callback: Callable[[], None],
|
|
144
|
+
delay_first_run: float = 0.0,
|
|
135
145
|
) -> ScheduledTask:
|
|
136
146
|
"""Register a recurring task.
|
|
137
147
|
|
|
@@ -139,11 +149,12 @@ class TaskScheduler:
|
|
|
139
149
|
name: Unique task name (used in logs and status output).
|
|
140
150
|
interval_seconds: Minimum seconds between executions.
|
|
141
151
|
callback: Zero-argument callable to invoke.
|
|
152
|
+
delay_first_run: Seconds to wait before first execution (default 0 = immediate).
|
|
142
153
|
|
|
143
154
|
Returns:
|
|
144
155
|
The created ScheduledTask (caller may inspect it at runtime).
|
|
145
156
|
"""
|
|
146
|
-
task = ScheduledTask(name=name, interval_seconds=interval_seconds, callback=callback)
|
|
157
|
+
task = ScheduledTask(name=name, interval_seconds=interval_seconds, callback=callback, delay_first_run=delay_first_run)
|
|
147
158
|
with self._lock:
|
|
148
159
|
self._tasks.append(task)
|
|
149
160
|
logger.debug("Registered scheduled task '%s' every %.0fs", name, interval_seconds)
|
|
@@ -214,29 +225,50 @@ class TaskScheduler:
|
|
|
214
225
|
def make_memory_promotion_task(home: Path) -> Callable[[], None]:
|
|
215
226
|
"""Return a callback that runs an hourly memory promotion sweep.
|
|
216
227
|
|
|
217
|
-
|
|
218
|
-
|
|
228
|
+
The sweep runs in a dedicated background thread so it never blocks the
|
|
229
|
+
scheduler (and therefore never blocks watchdog pings or other scheduled
|
|
230
|
+
tasks). A ``threading.Event`` gate prevents overlapping sweeps.
|
|
231
|
+
|
|
232
|
+
The sweep is rate-limited to 50 promotions per run to bound I/O time.
|
|
219
233
|
|
|
220
234
|
Args:
|
|
221
235
|
home: Agent home directory containing the ``memory/`` subtree.
|
|
222
236
|
"""
|
|
237
|
+
_running = threading.Event()
|
|
223
238
|
|
|
224
|
-
def
|
|
225
|
-
|
|
239
|
+
def _sweep() -> None:
|
|
240
|
+
try:
|
|
241
|
+
from .memory_promoter import PromotionEngine
|
|
242
|
+
|
|
243
|
+
engine = PromotionEngine(home)
|
|
244
|
+
result = engine.sweep(limit=50)
|
|
245
|
+
if result.promoted:
|
|
246
|
+
logger.info(
|
|
247
|
+
"Memory promotion sweep: %d promoted of %d scanned",
|
|
248
|
+
len(result.promoted),
|
|
249
|
+
result.scanned,
|
|
250
|
+
)
|
|
251
|
+
else:
|
|
252
|
+
logger.debug(
|
|
253
|
+
"Memory promotion sweep: %d scanned, 0 promoted",
|
|
254
|
+
result.scanned,
|
|
255
|
+
)
|
|
256
|
+
except Exception as exc:
|
|
257
|
+
logger.error("Memory promotion sweep error: %s", exc)
|
|
258
|
+
finally:
|
|
259
|
+
_running.clear()
|
|
226
260
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
result.scanned,
|
|
239
|
-
)
|
|
261
|
+
def _run() -> None:
|
|
262
|
+
if _running.is_set():
|
|
263
|
+
logger.debug("Memory promotion sweep already running — skipping")
|
|
264
|
+
return
|
|
265
|
+
_running.set()
|
|
266
|
+
t = threading.Thread(
|
|
267
|
+
target=_sweep,
|
|
268
|
+
name="memory-promotion-sweep",
|
|
269
|
+
daemon=True,
|
|
270
|
+
)
|
|
271
|
+
t.start()
|
|
240
272
|
|
|
241
273
|
return _run
|
|
242
274
|
|
|
@@ -498,6 +530,7 @@ def build_scheduler(
|
|
|
498
530
|
name="memory_promotion_sweep",
|
|
499
531
|
interval_seconds=3600, # 1 hour
|
|
500
532
|
callback=make_memory_promotion_task(home),
|
|
533
|
+
delay_first_run=120, # let daemon stabilize before first sweep
|
|
501
534
|
)
|
|
502
535
|
|
|
503
536
|
scheduler.register(
|
|
@@ -138,14 +138,15 @@ def _tcp_check(name: str, host: str, port: int) -> dict[str, Any]:
|
|
|
138
138
|
def check_all_services() -> list[dict[str, Any]]:
|
|
139
139
|
"""Ping every known service and return a list of status dicts.
|
|
140
140
|
|
|
141
|
-
Environment variables override default URLs:
|
|
142
|
-
SKMEMORY_SKVECTOR_URL
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
141
|
+
Environment variables override default URLs (set any to "disabled" to skip):
|
|
142
|
+
SKMEMORY_SKVECTOR_URL — Qdrant REST base (default http://localhost:6333)
|
|
143
|
+
SKMEMORY_SKVECTOR_API_KEY — Qdrant API key (sent as ``api-key`` header)
|
|
144
|
+
SKMEMORY_SKGRAPH_HOST — FalkorDB host (default localhost)
|
|
145
|
+
SKMEMORY_SKGRAPH_PORT — FalkorDB port (default 6379)
|
|
146
|
+
SYNCTHING_API_URL — Syncthing REST (default http://localhost:8384)
|
|
147
|
+
SYNCTHING_API_KEY — Syncthing API key (optional)
|
|
148
|
+
SKCAPSTONE_DAEMON_URL — Daemon HTTP base (default http://localhost:9383)
|
|
149
|
+
SKCHAT_DAEMON_URL — SKChat daemon (default http://localhost:9385)
|
|
149
150
|
|
|
150
151
|
Returns:
|
|
151
152
|
List of dicts, each containing: name, url, status ("up"|"down"|"unknown"),
|
|
@@ -155,13 +156,20 @@ def check_all_services() -> list[dict[str, Any]]:
|
|
|
155
156
|
|
|
156
157
|
# -- SKVector (Qdrant) --------------------------------------------------
|
|
157
158
|
qdrant_base = os.environ.get("SKMEMORY_SKVECTOR_URL", "http://localhost:6333")
|
|
158
|
-
|
|
159
|
-
|
|
159
|
+
if qdrant_base.lower() != "disabled":
|
|
160
|
+
qdrant_url = qdrant_base.rstrip("/") + "/healthz"
|
|
161
|
+
qdrant_headers: dict[str, str] = {}
|
|
162
|
+
qdrant_api_key = os.environ.get("SKMEMORY_SKVECTOR_API_KEY", "")
|
|
163
|
+
if qdrant_api_key:
|
|
164
|
+
qdrant_headers["api-key"] = qdrant_api_key
|
|
165
|
+
results.append(_http_check("skvector (Qdrant)", qdrant_url, headers=qdrant_headers))
|
|
160
166
|
|
|
161
167
|
# -- SKGraph (FalkorDB) — TCP check on Redis protocol port ---------------
|
|
162
168
|
graph_host = os.environ.get("SKMEMORY_SKGRAPH_HOST", "localhost")
|
|
163
|
-
|
|
164
|
-
|
|
169
|
+
graph_port_str = os.environ.get("SKMEMORY_SKGRAPH_PORT", "6379")
|
|
170
|
+
if graph_host.lower() != "disabled":
|
|
171
|
+
graph_port = int(graph_port_str)
|
|
172
|
+
results.append(_tcp_check("skgraph (FalkorDB)", graph_host, graph_port))
|
|
165
173
|
|
|
166
174
|
# -- Syncthing -----------------------------------------------------------
|
|
167
175
|
syncthing_base = os.environ.get("SYNCTHING_API_URL", "http://localhost:8384")
|
|
@@ -186,8 +194,9 @@ def check_all_services() -> list[dict[str, Any]]:
|
|
|
186
194
|
|
|
187
195
|
# -- skchat daemon -------------------------------------------------------
|
|
188
196
|
chat_base = os.environ.get("SKCHAT_DAEMON_URL", "http://localhost:9385")
|
|
189
|
-
|
|
190
|
-
|
|
197
|
+
if chat_base.lower() != "disabled":
|
|
198
|
+
chat_url = chat_base.rstrip("/") + "/health"
|
|
199
|
+
results.append(_http_check("skchat daemon", chat_url))
|
|
191
200
|
|
|
192
201
|
return results
|
|
193
202
|
|
|
@@ -29,7 +29,7 @@ ECOSYSTEM_PACKAGES = [
|
|
|
29
29
|
{"name": "skcomm", "path": "skcomm", "tests": "skcomm/tests"},
|
|
30
30
|
{"name": "skchat", "path": "skchat", "tests": "skchat/tests"},
|
|
31
31
|
{"name": "skmemory", "path": "skmemory", "tests": "skmemory/tests"},
|
|
32
|
-
{"name": "cloud9
|
|
32
|
+
{"name": "cloud9", "path": "cloud9", "tests": "cloud9/tests"},
|
|
33
33
|
]
|
|
34
34
|
|
|
35
35
|
|