superlocalmemory 3.7.7 → 3.8.0
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/.claude-plugin/marketplace.json +1 -1
- package/ATTRIBUTION.md +1 -3
- package/CHANGELOG.md +85 -0
- package/README.md +199 -29
- package/package.json +4 -2
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin/CLAUDE.md +8 -8
- package/plugin/agents/slm-governance-advisor.md +80 -0
- package/plugin/agents/slm-loop-runner.md +71 -0
- package/plugin/agents/slm-memory-advisor.md +10 -5
- package/plugin/agents/slm-optimize-advisor.md +9 -3
- package/plugin/commands/slm-loop.md +31 -0
- package/plugin/hooks/hooks.json +79 -0
- package/plugin/requirements.txt +1 -1
- package/plugin/scripts/slm-launch +46 -7
- package/plugin/settings.json +9 -0
- package/plugin/skills/slm-cache/SKILL.md +9 -1
- package/plugin/skills/slm-compress/SKILL.md +8 -1
- package/plugin/skills/slm-governance/SKILL.md +248 -0
- package/plugin/skills/slm-graph/SKILL.md +17 -3
- package/plugin/skills/slm-loop/SKILL.md +99 -0
- package/plugin/skills/slm-mesh/SKILL.md +282 -0
- package/plugin/skills/slm-profile/SKILL.md +148 -0
- package/plugin/skills/slm-recall/SKILL.md +46 -10
- package/plugin/skills/slm-remember/SKILL.md +48 -1
- package/plugin/skills/slm-scope/SKILL.md +176 -0
- package/plugin/skills/slm-session/SKILL.md +24 -1
- package/plugin/skills/slm-status/SKILL.md +18 -1
- package/plugin-src/agents/slm-governance-advisor.md +80 -0
- package/plugin-src/agents/slm-loop-runner.md +71 -0
- package/plugin-src/agents/slm-memory-advisor.md +10 -5
- package/plugin-src/agents/slm-optimize-advisor.md +9 -3
- package/plugin-src/commands/slm-loop.md +31 -0
- package/plugin-src/hooks/hooks.json +79 -0
- package/plugin-src/manifest.json +7 -2
- package/plugin-src/requirements.txt +1 -1
- package/plugin-src/rules/AGENTS.md +57 -18
- package/plugin-src/rules/CLAUDE.md.fragment +8 -8
- package/plugin-src/scripts/slm-launch +46 -7
- package/plugin-src/settings.json +9 -0
- package/plugin-src/skills/slm-cache/SKILL.md +9 -1
- package/plugin-src/skills/slm-compress/SKILL.md +8 -1
- package/plugin-src/skills/slm-governance/SKILL.md +248 -0
- package/plugin-src/skills/slm-graph/SKILL.md +17 -3
- package/plugin-src/skills/slm-loop/SKILL.md +99 -0
- package/plugin-src/skills/slm-mesh/SKILL.md +282 -0
- package/plugin-src/skills/slm-profile/SKILL.md +148 -0
- package/plugin-src/skills/slm-recall/SKILL.md +46 -10
- package/plugin-src/skills/slm-remember/SKILL.md +48 -1
- package/plugin-src/skills/slm-scope/SKILL.md +176 -0
- package/plugin-src/skills/slm-session/SKILL.md +24 -1
- package/plugin-src/skills/slm-status/SKILL.md +18 -1
- package/pyproject.toml +1 -2
- package/scripts/postinstall/validation.js +2 -0
- package/scripts/postinstall-interactive.js +74 -2
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/access/__init__.py +3 -0
- package/src/superlocalmemory/access/rbac.py +477 -0
- package/src/superlocalmemory/cli/commands.py +96 -12
- package/src/superlocalmemory/cli/compress_cmd.py +17 -7
- package/src/superlocalmemory/cli/loop_cmd.py +192 -0
- package/src/superlocalmemory/cli/main.py +39 -4
- package/src/superlocalmemory/cli/mesh_cmd.py +38 -0
- package/src/superlocalmemory/cli/optimize_cmd.py +3 -0
- package/src/superlocalmemory/cli/pending_store.py +49 -13
- package/src/superlocalmemory/cli/proxy_cmd.py +4 -0
- package/src/superlocalmemory/cli/scale_engine_cmd.py +6 -0
- package/src/superlocalmemory/cli/setup_wizard.py +22 -13
- package/src/superlocalmemory/compliance/audit.py +6 -0
- package/src/superlocalmemory/compliance/gdpr.py +128 -138
- package/src/superlocalmemory/compliance/retention.py +176 -45
- package/src/superlocalmemory/core/backend_orchestrator.py +5 -43
- package/src/superlocalmemory/core/community_summary.py +267 -0
- package/src/superlocalmemory/core/config.py +216 -3
- package/src/superlocalmemory/core/consolidation_engine.py +95 -22
- package/src/superlocalmemory/core/context_cache.py +61 -18
- package/src/superlocalmemory/core/embedding_worker.py +17 -2
- package/src/superlocalmemory/core/embeddings.py +12 -1
- package/src/superlocalmemory/core/engine.py +17 -1
- package/src/superlocalmemory/core/engine_ingestion.py +29 -0
- package/src/superlocalmemory/core/engine_wiring.py +13 -0
- package/src/superlocalmemory/core/entity_community.py +178 -0
- package/src/superlocalmemory/core/graph_analyzer.py +39 -2
- package/src/superlocalmemory/core/graph_pruner.py +13 -8
- package/src/superlocalmemory/core/key_expander.py +138 -0
- package/src/superlocalmemory/core/maintenance.py +23 -0
- package/src/superlocalmemory/core/modes.py +1 -1
- package/src/superlocalmemory/core/mutations.py +2 -2
- package/src/superlocalmemory/core/pii.py +105 -0
- package/src/superlocalmemory/core/progressive_abstraction.py +208 -0
- package/src/superlocalmemory/core/recall_pipeline.py +2 -0
- package/src/superlocalmemory/core/recall_worker.py +20 -6
- package/src/superlocalmemory/core/scale_engine.py +60 -1
- package/src/superlocalmemory/core/security_primitives.py +40 -2
- package/src/superlocalmemory/core/store_pipeline.py +35 -11
- package/src/superlocalmemory/core/worker_pool.py +21 -6
- package/src/superlocalmemory/encoding/entity_reflexion.py +200 -0
- package/src/superlocalmemory/encoding/entity_resolver.py +34 -24
- package/src/superlocalmemory/encoding/fact_extractor.py +26 -1
- package/src/superlocalmemory/encoding/temporal_validator.py +64 -1
- package/src/superlocalmemory/evolution/evolution_store.py +122 -45
- package/src/superlocalmemory/evolution/llm_dispatch.py +12 -1
- package/src/superlocalmemory/evolution/model_selection.py +160 -0
- package/src/superlocalmemory/evolution/mutation_generator.py +16 -0
- package/src/superlocalmemory/evolution/skill_evolver.py +127 -42
- package/src/superlocalmemory/evolution/triggers.py +22 -13
- package/src/superlocalmemory/graph/cozo_backend.py +43 -20
- package/src/superlocalmemory/hooks/adapter_base.py +5 -1
- package/src/superlocalmemory/hooks/auto_recall.py +13 -1
- package/src/superlocalmemory/hooks/claude_code_hooks.py +11 -0
- package/src/superlocalmemory/hooks/codex_assets.py +64 -5
- package/src/superlocalmemory/hooks/hook_daemon.py +20 -3
- package/src/superlocalmemory/hooks/memory_protocol.py +54 -0
- package/src/superlocalmemory/hooks/portable_kit.py +114 -1
- package/src/superlocalmemory/infra/auth_middleware.py +28 -0
- package/src/superlocalmemory/infra/backup.py +12 -1
- package/src/superlocalmemory/infra/daemon_identity.py +40 -4
- package/src/superlocalmemory/infra/data_root.py +43 -4
- package/src/superlocalmemory/infra/event_bus.py +107 -24
- package/src/superlocalmemory/infra/rate_limiter.py +93 -0
- package/src/superlocalmemory/ingestion/adapter_manager.py +4 -1
- package/src/superlocalmemory/ingestion/credentials.py +1 -1
- package/src/superlocalmemory/learning/cross_project.py +28 -19
- package/src/superlocalmemory/learning/reward_proxy.py +42 -9
- package/src/superlocalmemory/loops/__init__.py +56 -0
- package/src/superlocalmemory/loops/budget.py +58 -0
- package/src/superlocalmemory/loops/engine.py +164 -0
- package/src/superlocalmemory/loops/ledger.py +243 -0
- package/src/superlocalmemory/loops/models.py +152 -0
- package/src/superlocalmemory/loops/rules.py +52 -0
- package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
- package/src/superlocalmemory/mcp/_pool_adapter.py +2 -0
- package/src/superlocalmemory/mcp/profiles.py +103 -0
- package/src/superlocalmemory/mcp/server.py +21 -49
- package/src/superlocalmemory/mcp/tools_active.py +4 -7
- package/src/superlocalmemory/mcp/tools_code_graph.py +51 -5
- package/src/superlocalmemory/mcp/tools_core.py +50 -5
- package/src/superlocalmemory/mcp/tools_evolution.py +6 -3
- package/src/superlocalmemory/mcp/tools_loops.py +300 -0
- package/src/superlocalmemory/mcp/tools_mesh.py +140 -4
- package/src/superlocalmemory/mcp/tools_optimize.py +15 -8
- package/src/superlocalmemory/mesh/broker.py +237 -129
- package/src/superlocalmemory/mesh/remote_sync.py +50 -8
- package/src/superlocalmemory/optimize/NOTICE +1 -6
- package/src/superlocalmemory/optimize/adapters/anthropic_adapter.py +1 -4
- package/src/superlocalmemory/optimize/adapters/openai_adapter.py +1 -4
- package/src/superlocalmemory/optimize/cache/semantic.py +27 -19
- package/src/superlocalmemory/optimize/compress/align.py +32 -26
- package/src/superlocalmemory/optimize/compress/ccr.py +14 -71
- package/src/superlocalmemory/optimize/compress/router.py +105 -22
- package/src/superlocalmemory/optimize/config/defaults.py +1 -1
- package/src/superlocalmemory/optimize/config/schema.py +87 -4
- package/src/superlocalmemory/optimize/metrics/counters.py +13 -4
- package/src/superlocalmemory/optimize/metrics/estimator.py +0 -3
- package/src/superlocalmemory/optimize/proxy/_helpers.py +31 -4
- package/src/superlocalmemory/optimize/storage/db.py +38 -9
- package/src/superlocalmemory/optimize/storage/schema.py +10 -0
- package/src/superlocalmemory/parameterization/pattern_extractor.py +6 -3
- package/src/superlocalmemory/retrieval/agentic.py +1 -1
- package/src/superlocalmemory/retrieval/bm25_channel.py +68 -10
- package/src/superlocalmemory/retrieval/engine.py +168 -26
- package/src/superlocalmemory/retrieval/entity_channel.py +7 -5
- package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -2
- package/src/superlocalmemory/retrieval/semantic_channel.py +114 -21
- package/src/superlocalmemory/retrieval/spreading_activation.py +11 -2
- package/src/superlocalmemory/retrieval/temporal_channel.py +48 -9
- package/src/superlocalmemory/retrieval/temporal_frame.py +102 -0
- package/src/superlocalmemory/retrieval/temporal_validity_filter.py +135 -0
- package/src/superlocalmemory/retrieval/time_window.py +181 -0
- package/src/superlocalmemory/server/api.py +21 -4
- package/src/superlocalmemory/server/profile_runtime.py +125 -8
- package/src/superlocalmemory/server/rbac_enforce.py +142 -0
- package/src/superlocalmemory/server/recall_health.py +24 -3
- package/src/superlocalmemory/server/recall_serializer.py +19 -1
- package/src/superlocalmemory/server/routes/abstraction.py +115 -0
- package/src/superlocalmemory/server/routes/agents.py +128 -38
- package/src/superlocalmemory/server/routes/backup.py +34 -10
- package/src/superlocalmemory/server/routes/behavioral.py +13 -12
- package/src/superlocalmemory/server/routes/brain.py +21 -5
- package/src/superlocalmemory/server/routes/chat.py +72 -16
- package/src/superlocalmemory/server/routes/compliance.py +171 -21
- package/src/superlocalmemory/server/routes/config_api.py +436 -0
- package/src/superlocalmemory/server/routes/data_io.py +30 -8
- package/src/superlocalmemory/server/routes/entity.py +9 -4
- package/src/superlocalmemory/server/routes/events.py +24 -8
- package/src/superlocalmemory/server/routes/evolution.py +135 -17
- package/src/superlocalmemory/server/routes/helpers.py +16 -1
- package/src/superlocalmemory/server/routes/ingest.py +7 -4
- package/src/superlocalmemory/server/routes/insights.py +3 -3
- package/src/superlocalmemory/server/routes/learning.py +14 -14
- package/src/superlocalmemory/server/routes/lifecycle.py +59 -8
- package/src/superlocalmemory/server/routes/memories.py +221 -49
- package/src/superlocalmemory/server/routes/mesh.py +95 -15
- package/src/superlocalmemory/server/routes/optimize.py +33 -1
- package/src/superlocalmemory/server/routes/prewarm.py +2 -0
- package/src/superlocalmemory/server/routes/profiles.py +63 -17
- package/src/superlocalmemory/server/routes/ratelimit.py +124 -0
- package/src/superlocalmemory/server/routes/rbac.py +367 -0
- package/src/superlocalmemory/server/routes/stats.py +13 -6
- package/src/superlocalmemory/server/routes/tiers.py +11 -9
- package/src/superlocalmemory/server/routes/v3_api.py +194 -81
- package/src/superlocalmemory/server/routes/ws.py +5 -2
- package/src/superlocalmemory/server/security_middleware.py +12 -5
- package/src/superlocalmemory/server/ui.py +30 -5
- package/src/superlocalmemory/server/unified_daemon.py +431 -75
- package/src/superlocalmemory/server/write_identity.py +38 -8
- package/src/superlocalmemory/storage/database.py +265 -53
- package/src/superlocalmemory/storage/migration_runner.py +53 -0
- package/src/superlocalmemory/storage/migrations/M021_ingestion_log_profile.py +108 -0
- package/src/superlocalmemory/storage/migrations/M022_entity_aliases_profile.py +86 -0
- package/src/superlocalmemory/storage/migrations/M023_mesh_profile_isolation.py +194 -0
- package/src/superlocalmemory/storage/migrations/M024_rbac_users_roles.py +87 -0
- package/src/superlocalmemory/storage/migrations/M025_perf_indexes.py +90 -0
- package/src/superlocalmemory/storage/migrations/M026_rbac_memberships_fk.py +136 -0
- package/src/superlocalmemory/storage/migrations/M027_transferable_patterns_profile.py +163 -0
- package/src/superlocalmemory/storage/models.py +4 -0
- package/src/superlocalmemory/storage/schema.py +87 -0
- package/src/superlocalmemory/storage/schema_v32.py +0 -9
- package/src/superlocalmemory/storage/schema_v343.py +24 -12
- package/src/superlocalmemory/trust/gate.py +49 -8
- package/src/superlocalmemory/ui/assets/slm-icon-white.svg +64 -0
- package/src/superlocalmemory/ui/assets/slm-icon.svg +36 -0
- package/src/superlocalmemory/ui/css/design-system.css +621 -0
- package/src/superlocalmemory/ui/css/neural-glass.css +6 -0
- package/src/superlocalmemory/ui/css/od-bridge.css +158 -0
- package/src/superlocalmemory/ui/favicon.svg +35 -4
- package/src/superlocalmemory/ui/index.html +306 -173
- package/src/superlocalmemory/ui/js/brain.js +5 -20
- package/src/superlocalmemory/ui/js/core.js +47 -31
- package/src/superlocalmemory/ui/js/dashboard.js +314 -63
- package/src/superlocalmemory/ui/js/event-delegation.js +102 -0
- package/src/superlocalmemory/ui/js/knowledge-graph.js +11 -11
- package/src/superlocalmemory/ui/js/math-health.js +1 -1
- package/src/superlocalmemory/ui/js/memories.js +15 -4
- package/src/superlocalmemory/ui/js/memory-chat.js +7 -7
- package/src/superlocalmemory/ui/js/ng-entities.js +6 -8
- package/src/superlocalmemory/ui/js/ng-ingestion.js +4 -4
- package/src/superlocalmemory/ui/js/ng-mesh.js +4 -9
- package/src/superlocalmemory/ui/js/ng-shell.js +8 -8
- package/src/superlocalmemory/ui/js/ng-skills.js +54 -2
- package/src/superlocalmemory/ui/js/od-agents.js +544 -0
- package/src/superlocalmemory/ui/js/od-auth-gate.js +257 -0
- package/src/superlocalmemory/ui/js/od-backup.js +780 -0
- package/src/superlocalmemory/ui/js/od-brain.js +779 -0
- package/src/superlocalmemory/ui/js/od-entities.js +579 -0
- package/src/superlocalmemory/ui/js/od-graph.js +593 -0
- package/src/superlocalmemory/ui/js/od-health.js +539 -0
- package/src/superlocalmemory/ui/js/od-mcp.js +508 -0
- package/src/superlocalmemory/ui/js/od-memories.js +887 -0
- package/src/superlocalmemory/ui/js/od-mesh.js +539 -0
- package/src/superlocalmemory/ui/js/od-operations.js +1250 -0
- package/src/superlocalmemory/ui/js/od-optimize.js +787 -0
- package/src/superlocalmemory/ui/js/od-settings.js +1053 -0
- package/src/superlocalmemory/ui/js/od-shell.js +593 -0
- package/src/superlocalmemory/ui/js/od-skills.js +573 -0
- package/src/superlocalmemory/ui/js/od-team.js +258 -0
- package/src/superlocalmemory/ui/js/profiles.js +159 -46
- package/src/superlocalmemory/ui/js/settings.js +2 -2
- package/src/superlocalmemory/ui/js/timeline.js +34 -5
- package/src/superlocalmemory/ui/js/trust-dashboard.js +2 -2
- package/src/superlocalmemory/vector/lancedb_backend.py +8 -6
- package/src/superlocalmemory/learning/behavioral_listener.py +0 -94
|
@@ -46,27 +46,34 @@ from superlocalmemory.evolution.budget import (
|
|
|
46
46
|
EvolutionBudget,
|
|
47
47
|
)
|
|
48
48
|
from superlocalmemory.evolution.llm_dispatch import _dispatch_llm
|
|
49
|
+
from superlocalmemory.evolution.model_selection import (
|
|
50
|
+
_MODEL_ALIASES,
|
|
51
|
+
_resolve_model_alias,
|
|
52
|
+
resolve_evolution_models,
|
|
53
|
+
)
|
|
49
54
|
|
|
50
55
|
logger = logging.getLogger(__name__)
|
|
51
56
|
|
|
52
57
|
EVOLVED_SKILLS_DIR = Path.home() / ".claude" / "skills" / "evolved"
|
|
53
58
|
|
|
54
|
-
#
|
|
55
|
-
#
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
"ollama": "ollama:llama3",
|
|
60
|
-
"ollama:llama3": "ollama:llama3",
|
|
61
|
-
"ollama:qwen2.5": "ollama:qwen2.5",
|
|
62
|
-
"claude-haiku-4-5": "claude-haiku-4-5",
|
|
63
|
-
"claude-sonnet-4-6": "claude-sonnet-4-6",
|
|
64
|
-
}
|
|
59
|
+
# Model aliasing + per-step selection live in ``model_selection`` — a single
|
|
60
|
+
# source of truth shared with the config layer. ``_MODEL_ALIASES`` and
|
|
61
|
+
# ``_resolve_model_alias`` are imported above and re-exported here for
|
|
62
|
+
# callers/tests that still reference them via ``skill_evolver``.
|
|
63
|
+
__all__ = ["SkillEvolver", "detect_backend", "_resolve_model_alias"]
|
|
65
64
|
|
|
66
65
|
|
|
67
|
-
def
|
|
68
|
-
"""
|
|
69
|
-
|
|
66
|
+
def _ollama_running() -> bool:
|
|
67
|
+
"""Return True if a local Ollama daemon answers on the default port."""
|
|
68
|
+
try:
|
|
69
|
+
import urllib.request
|
|
70
|
+
req = urllib.request.Request(
|
|
71
|
+
"http://127.0.0.1:11434/api/tags", method="GET",
|
|
72
|
+
)
|
|
73
|
+
with urllib.request.urlopen(req, timeout=2): # noqa: S310
|
|
74
|
+
return True
|
|
75
|
+
except Exception:
|
|
76
|
+
return False
|
|
70
77
|
|
|
71
78
|
|
|
72
79
|
def detect_backend() -> str:
|
|
@@ -81,13 +88,8 @@ def detect_backend() -> str:
|
|
|
81
88
|
return "claude"
|
|
82
89
|
|
|
83
90
|
# 2. Ollama running?
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
req = urllib.request.Request("http://127.0.0.1:11434/api/tags", method="GET")
|
|
87
|
-
with urllib.request.urlopen(req, timeout=2):
|
|
88
|
-
return "ollama"
|
|
89
|
-
except Exception:
|
|
90
|
-
pass
|
|
91
|
+
if _ollama_running():
|
|
92
|
+
return "ollama"
|
|
91
93
|
|
|
92
94
|
# 3. API key set?
|
|
93
95
|
if os.environ.get("ANTHROPIC_API_KEY"):
|
|
@@ -119,9 +121,11 @@ class SkillEvolver:
|
|
|
119
121
|
self._db_path = str(db_path)
|
|
120
122
|
self._store = EvolutionStore(db_path)
|
|
121
123
|
self._degradation = DegradationTrigger(db_path)
|
|
122
|
-
self._health = HealthCheckTrigger(db_path)
|
|
124
|
+
self._health = HealthCheckTrigger(db_path, profile_id=profile_id)
|
|
123
125
|
self._config = config
|
|
124
126
|
self._backend: str | None = None
|
|
127
|
+
# ResolvedModels, resolved lazily & cached via _get_models().
|
|
128
|
+
self._models = None
|
|
125
129
|
self._profile_id = profile_id
|
|
126
130
|
self._current_cycle_id: str | None = None
|
|
127
131
|
|
|
@@ -160,6 +164,26 @@ class SkillEvolver:
|
|
|
160
164
|
logger.info("Evolution backend: %s", self._backend)
|
|
161
165
|
return self._backend
|
|
162
166
|
|
|
167
|
+
def _get_models(self):
|
|
168
|
+
"""Resolve per-step evolution models (cached).
|
|
169
|
+
|
|
170
|
+
Defaults to the lowest-cost model for the active backend and keeps
|
|
171
|
+
the blind verifier independent of the generator where possible
|
|
172
|
+
(see ``model_selection.resolve_evolution_models``). Falls back to
|
|
173
|
+
stock ``EvolutionConfig`` defaults when no config is attached.
|
|
174
|
+
"""
|
|
175
|
+
if self._models is not None:
|
|
176
|
+
return self._models
|
|
177
|
+
backend = self._get_backend()
|
|
178
|
+
evo_cfg = getattr(self._config, "evolution", None)
|
|
179
|
+
if evo_cfg is None:
|
|
180
|
+
from superlocalmemory.core.config import EvolutionConfig
|
|
181
|
+
evo_cfg = EvolutionConfig()
|
|
182
|
+
self._models = resolve_evolution_models(
|
|
183
|
+
evo_cfg, backend, ollama_available=_ollama_running(),
|
|
184
|
+
)
|
|
185
|
+
return self._models
|
|
186
|
+
|
|
163
187
|
def run_consolidation_cycle(self, profile_id: str = "default") -> dict:
|
|
164
188
|
"""Run during consolidation. Checks triggers 2 and 3.
|
|
165
189
|
|
|
@@ -200,7 +224,7 @@ class SkillEvolver:
|
|
|
200
224
|
self, profile_id: str, backend: str,
|
|
201
225
|
) -> dict:
|
|
202
226
|
"""Inner consolidation loop — runs under an open budget cycle."""
|
|
203
|
-
self._store.reset_cycle()
|
|
227
|
+
self._store.reset_cycle(profile_id)
|
|
204
228
|
results = {"candidates": 0, "evolved": 0, "rejected": 0, "skipped": 0, "backend": backend}
|
|
205
229
|
|
|
206
230
|
# Prune recovered skills from anti-loop tracking
|
|
@@ -215,7 +239,7 @@ class SkillEvolver:
|
|
|
215
239
|
results["candidates"] = len(candidates)
|
|
216
240
|
|
|
217
241
|
for candidate in candidates:
|
|
218
|
-
if not self._store.can_evolve():
|
|
242
|
+
if not self._store.can_evolve(profile_id):
|
|
219
243
|
results["skipped"] += len(candidates) - results["evolved"] - results["rejected"]
|
|
220
244
|
break
|
|
221
245
|
|
|
@@ -271,7 +295,7 @@ class SkillEvolver:
|
|
|
271
295
|
results["candidates"] = len(candidates)
|
|
272
296
|
|
|
273
297
|
for candidate in candidates:
|
|
274
|
-
if not self._store.can_evolve():
|
|
298
|
+
if not self._store.can_evolve(profile_id):
|
|
275
299
|
break
|
|
276
300
|
outcome = self._process_candidate(candidate, profile_id)
|
|
277
301
|
if outcome == "evolved":
|
|
@@ -281,6 +305,30 @@ class SkillEvolver:
|
|
|
281
305
|
|
|
282
306
|
return results
|
|
283
307
|
|
|
308
|
+
def evolve_candidate(
|
|
309
|
+
self, candidate: EvolutionCandidate, profile_id: str = "default",
|
|
310
|
+
) -> str:
|
|
311
|
+
"""Evolve a single candidate under a budget cycle (audit-10 fix).
|
|
312
|
+
|
|
313
|
+
Public entry point for the ``evolve_skill`` MCP tool. Opening the
|
|
314
|
+
budget cycle HERE is what makes the per-cycle LLM-call, wall-time and
|
|
315
|
+
per-day caps apply to manual/MCP-triggered evolution, exactly as they
|
|
316
|
+
do to consolidation. The MCP tool previously called
|
|
317
|
+
``_process_candidate`` directly, so budget charges happened outside a
|
|
318
|
+
cycle, the guard raised RuntimeError, ``_llm_call`` swallowed it, and
|
|
319
|
+
every cap was silently bypassed.
|
|
320
|
+
"""
|
|
321
|
+
try:
|
|
322
|
+
with self._budget.cycle():
|
|
323
|
+
self._current_cycle_id = None
|
|
324
|
+
return self._process_candidate(candidate, profile_id)
|
|
325
|
+
except BudgetExhausted as exc:
|
|
326
|
+
logger.info(
|
|
327
|
+
"evolve_candidate skipped: budget exhausted [%s]",
|
|
328
|
+
getattr(exc, "dimension", "?"),
|
|
329
|
+
)
|
|
330
|
+
return "skipped"
|
|
331
|
+
|
|
284
332
|
def _process_candidate(
|
|
285
333
|
self, candidate: EvolutionCandidate, profile_id: str,
|
|
286
334
|
) -> str:
|
|
@@ -301,7 +349,7 @@ class SkillEvolver:
|
|
|
301
349
|
if self._store.is_addressed(candidate.skill_name, context_hash):
|
|
302
350
|
return "skipped"
|
|
303
351
|
|
|
304
|
-
if self._store.has_exceeded_attempts(candidate.skill_name):
|
|
352
|
+
if self._store.has_exceeded_attempts(candidate.skill_name, profile_id):
|
|
305
353
|
logger.info("Skill %s exceeded max attempts, flagging for review", candidate.skill_name)
|
|
306
354
|
return "skipped"
|
|
307
355
|
|
|
@@ -323,7 +371,7 @@ class SkillEvolver:
|
|
|
323
371
|
original_content=original_content[:2000],
|
|
324
372
|
created_at=now,
|
|
325
373
|
)
|
|
326
|
-
self._store.save_record(record)
|
|
374
|
+
self._store.save_record(record, profile_id)
|
|
327
375
|
|
|
328
376
|
# Step 2: LLM confirmation gate (uses Haiku for cost)
|
|
329
377
|
confirmed = self._llm_confirm(candidate, original_content)
|
|
@@ -334,7 +382,7 @@ class SkillEvolver:
|
|
|
334
382
|
rejection_reason="LLM confirmation gate rejected",
|
|
335
383
|
completed_at=datetime.now(timezone.utc).isoformat(),
|
|
336
384
|
)
|
|
337
|
-
self._store.save_record(record)
|
|
385
|
+
self._store.save_record(record, profile_id)
|
|
338
386
|
return "rejected"
|
|
339
387
|
|
|
340
388
|
# Step 3: Generate mutation (uses Sonnet for quality)
|
|
@@ -347,7 +395,7 @@ class SkillEvolver:
|
|
|
347
395
|
rejection_reason="Mutation generation failed",
|
|
348
396
|
completed_at=datetime.now(timezone.utc).isoformat(),
|
|
349
397
|
)
|
|
350
|
-
self._store.save_record(record)
|
|
398
|
+
self._store.save_record(record, profile_id)
|
|
351
399
|
return "rejected"
|
|
352
400
|
|
|
353
401
|
# Step 4: Blind verification (uses Haiku — different model from generator)
|
|
@@ -365,7 +413,7 @@ class SkillEvolver:
|
|
|
365
413
|
blind_verified=False,
|
|
366
414
|
completed_at=datetime.now(timezone.utc).isoformat(),
|
|
367
415
|
)
|
|
368
|
-
self._store.save_record(record)
|
|
416
|
+
self._store.save_record(record, profile_id)
|
|
369
417
|
return "rejected"
|
|
370
418
|
|
|
371
419
|
# Step 5: Persist evolved skill
|
|
@@ -373,7 +421,9 @@ class SkillEvolver:
|
|
|
373
421
|
skill_path = self._write_evolved_skill(candidate, evolved_content, record_id)
|
|
374
422
|
|
|
375
423
|
# M-GENERATION: Compute generation from parent's history
|
|
376
|
-
parent_history = self._store.get_skill_history(
|
|
424
|
+
parent_history = self._store.get_skill_history(
|
|
425
|
+
candidate.skill_name, profile_id, limit=1,
|
|
426
|
+
)
|
|
377
427
|
parent_gen = (
|
|
378
428
|
parent_history[0].generation
|
|
379
429
|
if parent_history and parent_history[0].status == EvolutionStatus.PROMOTED
|
|
@@ -390,8 +440,8 @@ class SkillEvolver:
|
|
|
390
440
|
generation=parent_gen + 1,
|
|
391
441
|
completed_at=datetime.now(timezone.utc).isoformat(),
|
|
392
442
|
)
|
|
393
|
-
self._store.save_record(record)
|
|
394
|
-
self._store.record_evolution_attempt()
|
|
443
|
+
self._store.save_record(record, profile_id)
|
|
444
|
+
self._store.record_evolution_attempt(profile_id)
|
|
395
445
|
|
|
396
446
|
logger.info(
|
|
397
447
|
"Evolved skill: %s (%s via %s) → %s",
|
|
@@ -451,8 +501,20 @@ class SkillEvolver:
|
|
|
451
501
|
cycle_id=self._current_cycle_id,
|
|
452
502
|
)
|
|
453
503
|
except ValueError as exc:
|
|
454
|
-
#
|
|
455
|
-
|
|
504
|
+
# A ValueError from _dispatch_llm is a CONTRACT breach, not a
|
|
505
|
+
# runtime/transport failure: forbidden or unlisted model,
|
|
506
|
+
# non-positive max_tokens, max_tokens over MAX_TOKENS_CAP, or
|
|
507
|
+
# an empty profile_id. Every one is a misconfiguration in our
|
|
508
|
+
# own wiring — e.g. a caller requesting more tokens than the
|
|
509
|
+
# ceiling, which is exactly how mutation generation died
|
|
510
|
+
# silently for releases. Log at ERROR so it surfaces in logs
|
|
511
|
+
# and CI instead of masquerading as a normal "no LLM"
|
|
512
|
+
# fail-closed. Behaviour is unchanged: we still return "" so a
|
|
513
|
+
# consolidation cycle degrades gracefully rather than crashing.
|
|
514
|
+
logger.error(
|
|
515
|
+
"evolution dispatch misconfigured — no evolution this "
|
|
516
|
+
"call (returning empty string): %s", exc,
|
|
517
|
+
)
|
|
456
518
|
return ""
|
|
457
519
|
except Exception as exc: # noqa: BLE001 — fail-closed
|
|
458
520
|
logger.debug("evolution dispatch failed: %s", exc)
|
|
@@ -467,16 +529,25 @@ class SkillEvolver:
|
|
|
467
529
|
f"Should this skill be evolved ({candidate.evolution_type.value})? "
|
|
468
530
|
f"Reply YES or NO with brief reason."
|
|
469
531
|
)
|
|
470
|
-
response = self._llm_call(
|
|
532
|
+
response = self._llm_call(
|
|
533
|
+
prompt, max_tokens=100, model=self._get_models().confirm,
|
|
534
|
+
)
|
|
471
535
|
if not response:
|
|
472
536
|
logger.warning("LLM confirmation gate: empty response, skipping evolution for %s", candidate.skill_name)
|
|
473
537
|
return False # Fail-closed: no LLM = no evolution
|
|
474
538
|
return "yes" in response.lower()
|
|
475
539
|
|
|
476
540
|
def _generate_mutation(self, prompt: str) -> Optional[str]:
|
|
477
|
-
"""Generate evolved SKILL.md
|
|
541
|
+
"""Generate evolved SKILL.md via the configured mutation model.
|
|
542
|
+
|
|
543
|
+
Defaults to the lowest-cost model for the backend (v3.7.9);
|
|
544
|
+
users can opt up (e.g. to sonnet) via ``evolution.mutation_model``.
|
|
545
|
+
"""
|
|
546
|
+
mutation_model = self._get_models().mutation
|
|
478
547
|
for attempt in range(mutgen.MAX_APPLY_RETRIES):
|
|
479
|
-
response = self._llm_call(
|
|
548
|
+
response = self._llm_call(
|
|
549
|
+
prompt, max_tokens=4000, model=mutation_model,
|
|
550
|
+
)
|
|
480
551
|
if not response:
|
|
481
552
|
return None
|
|
482
553
|
content = mutgen.parse_mutation_output(response)
|
|
@@ -492,8 +563,15 @@ class SkillEvolver:
|
|
|
492
563
|
return None
|
|
493
564
|
|
|
494
565
|
def _blind_verify(self, prompt: str) -> verifier.VerificationResult:
|
|
495
|
-
"""Blind verification.
|
|
496
|
-
|
|
566
|
+
"""Blind verification — runs on a model independent of the generator.
|
|
567
|
+
|
|
568
|
+
See ``model_selection.resolve_evolution_models``: the verify model
|
|
569
|
+
is kept different from the mutation model where possible so the
|
|
570
|
+
generator can't grade its own homework.
|
|
571
|
+
"""
|
|
572
|
+
response = self._llm_call(
|
|
573
|
+
prompt, max_tokens=500, model=self._get_models().verify,
|
|
574
|
+
)
|
|
497
575
|
if not response:
|
|
498
576
|
return verifier.VerificationResult(
|
|
499
577
|
passed=False, confidence=0.0, reasoning="No LLM response",
|
|
@@ -545,8 +623,11 @@ class SkillEvolver:
|
|
|
545
623
|
"""Write evolved SKILL.md to ~/.claude/skills/evolved/."""
|
|
546
624
|
EVOLVED_SKILLS_DIR.mkdir(parents=True, exist_ok=True)
|
|
547
625
|
|
|
548
|
-
# Build directory name
|
|
549
|
-
|
|
626
|
+
# Build directory name. skill_name derives from a behavioral-assertion
|
|
627
|
+
# trigger (any non-whitespace), so sanitize for ALL evolution types —
|
|
628
|
+
# "../../.claude/settings" must not escape EVOLVED_SKILLS_DIR.
|
|
629
|
+
base_name = re.sub(r"[^a-zA-Z0-9_-]", "-",
|
|
630
|
+
candidate.skill_name).lower()[:50] or "skill"
|
|
550
631
|
if candidate.evolution_type == EvolutionType.FIX:
|
|
551
632
|
dir_name = f"{base_name}-v{record_id[:6]}"
|
|
552
633
|
elif candidate.evolution_type == EvolutionType.DERIVED:
|
|
@@ -556,8 +637,12 @@ class SkillEvolver:
|
|
|
556
637
|
dir_name = re.sub(r"[^a-zA-Z0-9_-]", "-", dir_name).lower()[:50]
|
|
557
638
|
else:
|
|
558
639
|
dir_name = base_name
|
|
640
|
+
dir_name = dir_name or "skill"
|
|
559
641
|
|
|
560
642
|
skill_dir = EVOLVED_SKILLS_DIR / dir_name
|
|
643
|
+
# Defense in depth: never write outside the evolved-skills directory.
|
|
644
|
+
if not str(skill_dir.resolve()).startswith(str(EVOLVED_SKILLS_DIR.resolve())):
|
|
645
|
+
raise ValueError(f"evolved skill path escapes sandbox: {dir_name!r}")
|
|
561
646
|
skill_dir.mkdir(parents=True, exist_ok=True)
|
|
562
647
|
|
|
563
648
|
skill_path = skill_dir / "SKILL.md"
|
|
@@ -294,17 +294,19 @@ class HealthCheckTrigger:
|
|
|
294
294
|
|
|
295
295
|
_STATE_KEY = "health_check_cycle_count"
|
|
296
296
|
|
|
297
|
-
def __init__(self, db_path: str | Path):
|
|
297
|
+
def __init__(self, db_path: str | Path, profile_id: str = "default"):
|
|
298
298
|
self._db_path = str(db_path)
|
|
299
|
+
self._profile_id = profile_id or "default"
|
|
299
300
|
self._check_every_n = 3 # Every 3rd consolidation (~18h)
|
|
300
301
|
|
|
301
302
|
def _read_cycle_count(self) -> int:
|
|
302
|
-
"""Read persisted cycle count
|
|
303
|
+
"""Read this profile's persisted cycle count."""
|
|
303
304
|
conn = sqlite3.connect(self._db_path, timeout=10)
|
|
304
305
|
try:
|
|
305
306
|
row = conn.execute(
|
|
306
|
-
"SELECT value FROM evolution_cycle_state
|
|
307
|
-
|
|
307
|
+
"SELECT value FROM evolution_cycle_state "
|
|
308
|
+
"WHERE profile_id = ? AND key = ?",
|
|
309
|
+
(self._profile_id, self._STATE_KEY),
|
|
308
310
|
).fetchone()
|
|
309
311
|
return int(row[0]) if row else 0
|
|
310
312
|
except sqlite3.OperationalError:
|
|
@@ -314,23 +316,30 @@ class HealthCheckTrigger:
|
|
|
314
316
|
conn.close()
|
|
315
317
|
|
|
316
318
|
def _write_cycle_count(self, count: int) -> None:
|
|
317
|
-
"""Persist cycle count
|
|
319
|
+
"""Persist this profile's cycle count.
|
|
318
320
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
+
The evolution_cycle_state table (profile_id, key) is created by
|
|
322
|
+
EvolutionStore with the correct composite-PK schema — we do NOT
|
|
323
|
+
CREATE it here (the old single-key CREATE produced a conflicting
|
|
324
|
+
schema and cross-profile collisions).
|
|
321
325
|
"""
|
|
322
326
|
conn = sqlite3.connect(self._db_path, timeout=10)
|
|
323
327
|
try:
|
|
324
|
-
#
|
|
328
|
+
# Correct composite-PK schema (matches EvolutionStore). Created only
|
|
329
|
+
# for standalone use (tests); a real deployment already has it.
|
|
325
330
|
conn.execute(
|
|
326
|
-
"CREATE TABLE IF NOT EXISTS evolution_cycle_state "
|
|
327
|
-
"
|
|
331
|
+
"CREATE TABLE IF NOT EXISTS evolution_cycle_state ("
|
|
332
|
+
"profile_id TEXT NOT NULL DEFAULT 'default', key TEXT NOT NULL, "
|
|
333
|
+
"value INTEGER DEFAULT 0, updated_at TEXT, "
|
|
334
|
+
"PRIMARY KEY (profile_id, key))",
|
|
328
335
|
)
|
|
329
336
|
now = datetime.now(timezone.utc).isoformat()
|
|
330
337
|
conn.execute(
|
|
331
|
-
"INSERT
|
|
332
|
-
"VALUES (?, ?, ?)"
|
|
333
|
-
(
|
|
338
|
+
"INSERT INTO evolution_cycle_state (profile_id, key, value, updated_at) "
|
|
339
|
+
"VALUES (?, ?, ?, ?) "
|
|
340
|
+
"ON CONFLICT(profile_id, key) DO UPDATE SET "
|
|
341
|
+
"value=excluded.value, updated_at=excluded.updated_at",
|
|
342
|
+
(self._profile_id, self._STATE_KEY, count, now),
|
|
334
343
|
)
|
|
335
344
|
conn.commit()
|
|
336
345
|
except sqlite3.OperationalError as exc:
|
|
@@ -672,32 +672,34 @@ class CozoDBGraphBackend:
|
|
|
672
672
|
# Tier Sync
|
|
673
673
|
# ------------------------------------------------------------------
|
|
674
674
|
|
|
675
|
+
# Rebind the whole entity row (Cozo has no partial ``:update`` op — that
|
|
676
|
+
# token does not parse) while binding the id/tier/timestamp as query
|
|
677
|
+
# parameters so an entity id containing a quote can never become Datalog.
|
|
678
|
+
# The ``*entity{...}`` match means non-existent ids are a safe no-op rather
|
|
679
|
+
# than creating a stub row.
|
|
680
|
+
_TIER_SYNC_QUERY = (
|
|
681
|
+
"?[id, name, entity_type, tier, properties, profile_id, created_at, updated_at] := "
|
|
682
|
+
"*entity{id, name, entity_type, properties, profile_id, created_at}, "
|
|
683
|
+
"id = $id, tier = $tier, updated_at = $now "
|
|
684
|
+
":put entity {id => name, entity_type, tier, properties, profile_id, created_at, updated_at}"
|
|
685
|
+
)
|
|
686
|
+
|
|
675
687
|
def sync_tier_changes(
|
|
676
688
|
self, added: list[str], removed: list[str]
|
|
677
689
|
) -> None:
|
|
678
|
-
"""Sync tier changes:
|
|
690
|
+
"""Sync tier changes: promote added entities to active, demote removed to cold."""
|
|
679
691
|
now = datetime.now().isoformat()
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
# Fetch entity data from existing CozoDB entities or set defaults
|
|
683
|
-
for entity_id in added:
|
|
684
|
-
try:
|
|
685
|
-
self._db.run(f"""
|
|
686
|
-
?[id, tier] <- [['{entity_id}', 'active']]
|
|
687
|
-
:update entity {{id => tier, updated_at: '{now}'}}
|
|
688
|
-
""")
|
|
689
|
-
except Exception:
|
|
690
|
-
pass
|
|
691
|
-
|
|
692
|
-
if removed:
|
|
693
|
-
for entity_id in removed:
|
|
692
|
+
for entity_ids, tier in ((added, "active"), (removed, "cold")):
|
|
693
|
+
for entity_id in entity_ids:
|
|
694
694
|
try:
|
|
695
|
-
self._db.run(
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
695
|
+
self._db.run(
|
|
696
|
+
self._TIER_SYNC_QUERY,
|
|
697
|
+
{"id": entity_id, "tier": tier, "now": now},
|
|
698
|
+
)
|
|
699
699
|
except Exception:
|
|
700
|
-
|
|
700
|
+
logger.debug(
|
|
701
|
+
"cozo tier sync skipped for entity %s", entity_id, exc_info=True
|
|
702
|
+
)
|
|
701
703
|
|
|
702
704
|
# ------------------------------------------------------------------
|
|
703
705
|
# Health Check
|
|
@@ -712,12 +714,20 @@ class CozoDBGraphBackend:
|
|
|
712
714
|
edge_count = self._db.run(
|
|
713
715
|
"?[count(from_id)] := *edge{from_id}"
|
|
714
716
|
)
|
|
717
|
+
# fact_entity bridge rows (fact_id, entity_id) — counted so parity
|
|
718
|
+
# verification can catch a silent bridge-import failure that would
|
|
719
|
+
# otherwise leave Cozo entity recall returning empty results.
|
|
720
|
+
fe_count = self._db.run(
|
|
721
|
+
"?[count(fact_id)] := *fact_entity{fact_id, entity_id}"
|
|
722
|
+
)
|
|
715
723
|
ec = entity_count.values.tolist()[0][0] if len(entity_count) > 0 else 0
|
|
716
724
|
edc = edge_count.values.tolist()[0][0] if len(edge_count) > 0 else 0
|
|
725
|
+
fec = fe_count.values.tolist()[0][0] if len(fe_count) > 0 else 0
|
|
717
726
|
return {
|
|
718
727
|
"status": "active",
|
|
719
728
|
"entities": int(ec),
|
|
720
729
|
"edges": int(edc),
|
|
730
|
+
"fact_entity": int(fec),
|
|
721
731
|
"shadow_checks": self._shadow_checks,
|
|
722
732
|
"shadow_mismatches": self._shadow_mismatches,
|
|
723
733
|
"shadow_errors": self._shadow_errors,
|
|
@@ -730,6 +740,19 @@ class CozoDBGraphBackend:
|
|
|
730
740
|
"db_path": self._db_path,
|
|
731
741
|
}
|
|
732
742
|
|
|
743
|
+
def entity_ids(self, limit: int = 1000) -> list[str]:
|
|
744
|
+
"""Return up to ``limit`` entity IDs, for scale-engine content parity.
|
|
745
|
+
|
|
746
|
+
Count parity alone cannot prove the import reproduced the right rows;
|
|
747
|
+
comparing this ID set against canonical SQLite catches an import that
|
|
748
|
+
landed the correct *number* of entities with wrong identities.
|
|
749
|
+
"""
|
|
750
|
+
try:
|
|
751
|
+
res = self._db.run("?[id] := *entity{id} :limit " + str(int(limit)))
|
|
752
|
+
return [row[0] for row in res.values.tolist()]
|
|
753
|
+
except Exception:
|
|
754
|
+
return []
|
|
755
|
+
|
|
733
756
|
# ------------------------------------------------------------------
|
|
734
757
|
# Rebuild (from SQLite canonical)
|
|
735
758
|
# ------------------------------------------------------------------
|
|
@@ -38,7 +38,11 @@ from typing import Protocol, runtime_checkable
|
|
|
38
38
|
# ---------------------------------------------------------------------------
|
|
39
39
|
|
|
40
40
|
HARD_BYTES_CAP = 4096
|
|
41
|
-
|
|
41
|
+
# Soft budget for the managed instruction block. Raised 2048 -> 2560 -> 2816 as
|
|
42
|
+
# the block grew to carry the memory, token-optimization, and (compact)
|
|
43
|
+
# bounded-loop protocols; the 4 KB hard cap still bounds total size (recall
|
|
44
|
+
# content is truncated to stay under it).
|
|
45
|
+
COPILOT_SOFT_BYTES = 2816
|
|
42
46
|
TRUNCATION_MARKER = b"\n<!-- truncated -->"
|
|
43
47
|
|
|
44
48
|
|
|
@@ -10,6 +10,7 @@ import logging
|
|
|
10
10
|
from typing import Any, Callable
|
|
11
11
|
|
|
12
12
|
from superlocalmemory.core.injection import InjectableMemory, render_context
|
|
13
|
+
from superlocalmemory.retrieval.temporal_frame import relative_age, temporal_frame
|
|
13
14
|
|
|
14
15
|
logger = logging.getLogger(__name__)
|
|
15
16
|
|
|
@@ -85,7 +86,13 @@ class AutoRecall:
|
|
|
85
86
|
)
|
|
86
87
|
for r in relevant[:self._max_memories]
|
|
87
88
|
]
|
|
88
|
-
|
|
89
|
+
ctx = render_context(memories, mode="B", cfg=None, wrap=True)
|
|
90
|
+
# T-inject: anchor the injected memories to "now" so a time-blind
|
|
91
|
+
# model can weigh recency. Prepend a one-line temporal frame.
|
|
92
|
+
frame = temporal_frame(
|
|
93
|
+
[getattr(r.fact, "created_at", "") for r in relevant[:self._max_memories]]
|
|
94
|
+
)
|
|
95
|
+
return f"{frame}\n\n{ctx}" if ctx else ctx
|
|
89
96
|
except Exception as exc:
|
|
90
97
|
logger.warning("Auto-recall failed: %s", exc)
|
|
91
98
|
return ""
|
|
@@ -104,12 +111,17 @@ class AutoRecall:
|
|
|
104
111
|
response = self._recall(query, self._max_memories)
|
|
105
112
|
if response is None:
|
|
106
113
|
return []
|
|
114
|
+
from datetime import datetime as _dt, timezone as _tz
|
|
115
|
+
_now = _dt.now(_tz.utc)
|
|
107
116
|
results = []
|
|
108
117
|
for r in response.results:
|
|
109
118
|
if r.score >= self._threshold:
|
|
119
|
+
_created = getattr(r.fact, "created_at", "") or ""
|
|
110
120
|
results.append({
|
|
111
121
|
"fact_id": r.fact.fact_id,
|
|
112
122
|
"content": r.fact.content[:300],
|
|
123
|
+
"created_at": _created,
|
|
124
|
+
"age_label": relative_age(_created, _now),
|
|
113
125
|
"score": round(r.score, 3),
|
|
114
126
|
"relevance_score": round(
|
|
115
127
|
getattr(r, "relevance_score", r.score) or 0.0, 3
|
|
@@ -261,6 +261,17 @@ def _hook_definitions(include_gate: bool = False) -> dict[str, list]:
|
|
|
261
261
|
"command": _wrap_python_cmd("stop_outcome"),
|
|
262
262
|
"timeout": 10000,
|
|
263
263
|
},
|
|
264
|
+
# Commit temporal summaries so session decisions survive beyond
|
|
265
|
+
# the git-state snapshot written by `slm hook stop`.
|
|
266
|
+
{
|
|
267
|
+
"type": "command",
|
|
268
|
+
"command": (
|
|
269
|
+
'cmd /c "slm session close 2>NUL || exit /b 0"'
|
|
270
|
+
if sys.platform == "win32"
|
|
271
|
+
else "slm session close 2>/dev/null || true"
|
|
272
|
+
),
|
|
273
|
+
"timeout": 15000,
|
|
274
|
+
},
|
|
264
275
|
]
|
|
265
276
|
}
|
|
266
277
|
],
|
|
@@ -7,10 +7,16 @@ import sysconfig
|
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
|
|
9
9
|
SKILLS = ("slm-cache", "slm-compress", "slm-graph", "slm-recall", "slm-remember", "slm-session", "slm-status")
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
|
|
11
|
+
# Codex subagent files written to ~/.codex/agents (content built by _agent_files()).
|
|
12
|
+
AGENTS = ("slm-memory-advisor.toml", "slm-optimize-advisor.toml")
|
|
13
|
+
|
|
14
|
+
_MEMORY_ADVISOR_TOML = (
|
|
15
|
+
'name = "slm-memory-advisor"\n'
|
|
16
|
+
'description = "Use SuperLocalMemory safely: initialize once, recall before remember, and store only durable atomic facts."\n'
|
|
17
|
+
'instructions = "Use SLM for memory discipline only. Check results before claiming success; preserve private scope unless the user explicitly asks to share."\n'
|
|
18
|
+
)
|
|
19
|
+
|
|
14
20
|
|
|
15
21
|
def _source_root() -> Path:
|
|
16
22
|
development = Path(__file__).resolve().parents[3] / "plugin-src" / "skills"
|
|
@@ -21,6 +27,57 @@ def _source_root() -> Path:
|
|
|
21
27
|
return installed
|
|
22
28
|
raise FileNotFoundError("Bundled Codex skills were not found in this installation")
|
|
23
29
|
|
|
30
|
+
|
|
31
|
+
def _agents_source_root() -> Path | None:
|
|
32
|
+
development = Path(__file__).resolve().parents[3] / "plugin-src" / "agents"
|
|
33
|
+
if development.exists():
|
|
34
|
+
return development
|
|
35
|
+
installed = Path(sysconfig.get_path("data")) / "share" / "superlocalmemory" / "codex" / "agents"
|
|
36
|
+
return installed if installed.exists() else None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _optimize_advisor_toml() -> str:
|
|
40
|
+
"""Build the optimize-advisor TOML from the canonical advisor doc so Codex
|
|
41
|
+
ships the FULL decision rules (the 8-rule tree), not a one-line stub. Falls
|
|
42
|
+
back to a short instruction only if the source doc is unavailable.
|
|
43
|
+
"""
|
|
44
|
+
description = (
|
|
45
|
+
"Apply SuperLocalMemory's no-proxy context-optimization rules — reversible "
|
|
46
|
+
"compression of large tool output and KV-caching of repeated reads/searches."
|
|
47
|
+
)
|
|
48
|
+
body = ""
|
|
49
|
+
root = _agents_source_root()
|
|
50
|
+
if root is not None:
|
|
51
|
+
src = root / "slm-optimize-advisor.md"
|
|
52
|
+
if src.exists():
|
|
53
|
+
text = src.read_text(encoding="utf-8")
|
|
54
|
+
if text.startswith("---"): # strip YAML frontmatter, keep the guidance body
|
|
55
|
+
end = text.find("\n---", 3)
|
|
56
|
+
if end != -1:
|
|
57
|
+
text = text[end + 4:]
|
|
58
|
+
body = text.strip()
|
|
59
|
+
if not body:
|
|
60
|
+
body = (
|
|
61
|
+
"Reduce context-window pressure with the Surface-B tools (reversible CCR "
|
|
62
|
+
"compression + a per-agent KV cache); fail-open — never block the task."
|
|
63
|
+
)
|
|
64
|
+
# TOML literal multi-line string ('''...'''): no escape processing, and the
|
|
65
|
+
# advisor body contains no ''' sequence.
|
|
66
|
+
return (
|
|
67
|
+
'name = "slm-optimize-advisor"\n'
|
|
68
|
+
f'description = "{description}"\n'
|
|
69
|
+
f"instructions = '''\n{body}\n'''\n"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _agent_files() -> dict:
|
|
74
|
+
"""Return {filename: TOML content} for the Codex subagents."""
|
|
75
|
+
return {
|
|
76
|
+
"slm-memory-advisor.toml": _MEMORY_ADVISOR_TOML,
|
|
77
|
+
"slm-optimize-advisor.toml": _optimize_advisor_toml(),
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
24
81
|
def install_assets(*, home: Path | None = None, dry_run: bool = False) -> dict:
|
|
25
82
|
"""Copy only named SLM assets; never rewrite user-owned assets."""
|
|
26
83
|
home = home or Path.home()
|
|
@@ -37,10 +94,11 @@ def install_assets(*, home: Path | None = None, dry_run: bool = False) -> dict:
|
|
|
37
94
|
target = skills_root / skill
|
|
38
95
|
target.mkdir(parents=True, exist_ok=True)
|
|
39
96
|
shutil.copy2(source / skill / "SKILL.md", target / "SKILL.md")
|
|
40
|
-
for filename, content in
|
|
97
|
+
for filename, content in _agent_files().items():
|
|
41
98
|
(agents_root / filename).write_text(content, encoding="utf-8")
|
|
42
99
|
return {"success": True, "skills": list(SKILLS), "agents": list(AGENTS), "dry_run": False}
|
|
43
100
|
|
|
101
|
+
|
|
44
102
|
def remove_assets(*, home: Path | None = None, dry_run: bool = False) -> dict:
|
|
45
103
|
"""Remove only the known SLM directories and files."""
|
|
46
104
|
home = home or Path.home()
|
|
@@ -52,6 +110,7 @@ def remove_assets(*, home: Path | None = None, dry_run: bool = False) -> dict:
|
|
|
52
110
|
shutil.rmtree(target) if target.is_dir() else target.unlink()
|
|
53
111
|
return {"success": True, "removed": [str(x) for x in existing], "dry_run": dry_run}
|
|
54
112
|
|
|
113
|
+
|
|
55
114
|
def status_assets(*, home: Path | None = None) -> dict:
|
|
56
115
|
home = home or Path.home()
|
|
57
116
|
skills = [x for x in SKILLS if (home / ".agents" / "skills" / x / "SKILL.md").exists()]
|