superlocalmemory 3.7.8 → 3.8.1
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 +129 -0
- package/README.md +205 -123
- package/package.json +12 -3
- package/plugin/.claude-plugin/plugin.json +2 -3
- 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/rules/AGENTS.md +57 -18
- 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-graph/SKILL.md +17 -3
- 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-session/SKILL.md +24 -1
- package/plugin-src/skills/slm-status/SKILL.md +18 -1
- package/pyproject.toml +2 -1
- 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 +228 -17
- package/src/superlocalmemory/cli/compress_cmd.py +17 -7
- package/src/superlocalmemory/cli/daemon.py +7 -0
- package/src/superlocalmemory/cli/loop_cmd.py +187 -0
- package/src/superlocalmemory/cli/main.py +49 -8
- 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/cli/version_banner.py +17 -3
- 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 +23 -59
- 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 +21 -7
- package/src/superlocalmemory/core/embeddings.py +131 -46
- package/src/superlocalmemory/core/engine.py +41 -22
- package/src/superlocalmemory/core/engine_ingestion.py +359 -43
- 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/ingestion_command.py +134 -25
- package/src/superlocalmemory/core/injection.py +12 -7
- package/src/superlocalmemory/core/key_expander.py +138 -0
- package/src/superlocalmemory/core/maintenance.py +23 -0
- package/src/superlocalmemory/core/maintenance_scheduler.py +17 -7
- 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 +7 -3
- 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 +186 -29
- 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/hook_handlers.py +6 -1
- package/src/superlocalmemory/hooks/memory_protocol.py +54 -0
- package/src/superlocalmemory/hooks/portable_kit.py +148 -3
- 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/model_rollback.py +3 -0
- package/src/superlocalmemory/learning/ranker_retrain_online.py +2 -0
- package/src/superlocalmemory/learning/reward.py +50 -0
- package/src/superlocalmemory/learning/reward_proxy.py +42 -9
- package/src/superlocalmemory/learning/source_quality.py +523 -1
- 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 +263 -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 +32 -79
- 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 +12 -4
- package/src/superlocalmemory/mcp/tools_evolution.py +6 -3
- package/src/superlocalmemory/mcp/tools_learning.py +2 -2
- 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 +221 -47
- package/src/superlocalmemory/retrieval/entity_channel.py +7 -5
- package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -2
- package/src/superlocalmemory/retrieval/reranker.py +3 -4
- 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 +4 -4
- package/src/superlocalmemory/server/config_file.py +90 -0
- package/src/superlocalmemory/server/origin.py +50 -0
- 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 +317 -70
- package/src/superlocalmemory/server/routes/behavioral.py +349 -71
- package/src/superlocalmemory/server/routes/brain.py +69 -12
- package/src/superlocalmemory/server/routes/chat.py +10 -5
- package/src/superlocalmemory/server/routes/compliance.py +171 -21
- package/src/superlocalmemory/server/routes/config_api.py +438 -0
- package/src/superlocalmemory/server/routes/data_io.py +30 -8
- package/src/superlocalmemory/server/routes/entity.py +108 -26
- package/src/superlocalmemory/server/routes/events.py +24 -8
- package/src/superlocalmemory/server/routes/evolution.py +189 -68
- 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 +289 -118
- package/src/superlocalmemory/server/routes/learning_telemetry.py +153 -0
- package/src/superlocalmemory/server/routes/lifecycle.py +59 -8
- package/src/superlocalmemory/server/routes/memories.py +182 -57
- package/src/superlocalmemory/server/routes/mesh.py +200 -31
- 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 +132 -0
- package/src/superlocalmemory/server/routes/rbac.py +367 -0
- package/src/superlocalmemory/server/routes/stats.py +103 -158
- package/src/superlocalmemory/server/routes/tiers.py +11 -9
- package/src/superlocalmemory/server/routes/token.py +3 -13
- package/src/superlocalmemory/server/routes/v3_api.py +247 -89
- package/src/superlocalmemory/server/routes/ws.py +5 -2
- package/src/superlocalmemory/server/security_middleware.py +12 -5
- package/src/superlocalmemory/server/ui.py +20 -5
- package/src/superlocalmemory/server/unified_daemon.py +827 -72
- package/src/superlocalmemory/server/write_identity.py +38 -8
- package/src/superlocalmemory/storage/database.py +265 -53
- package/src/superlocalmemory/storage/migration_runner.py +132 -1
- package/src/superlocalmemory/storage/migrations/M010_evolution_config.py +5 -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/migrations/M028_fact_entity_associations.py +270 -0
- package/src/superlocalmemory/storage/migrations/M029_behavioral_history_indexes.py +137 -0
- package/src/superlocalmemory/storage/migrations/M030_entity_explorer_indexes.py +93 -0
- package/src/superlocalmemory/storage/migrations/__init__.py +4 -0
- package/src/superlocalmemory/storage/models.py +4 -0
- package/src/superlocalmemory/storage/schema.py +136 -1
- package/src/superlocalmemory/storage/schema_v32.py +2 -0
- package/src/superlocalmemory/storage/schema_v343.py +24 -12
- package/src/superlocalmemory/storage/schema_v347.py +4 -0
- 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 +303 -173
- package/src/superlocalmemory/ui/js/brain.js +5 -20
- package/src/superlocalmemory/ui/js/core.js +100 -41
- package/src/superlocalmemory/ui/js/dashboard.js +403 -65
- 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 +871 -0
- package/src/superlocalmemory/ui/js/od-brain.js +816 -0
- package/src/superlocalmemory/ui/js/od-entities.js +579 -0
- package/src/superlocalmemory/ui/js/od-graph.js +600 -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 +929 -0
- package/src/superlocalmemory/ui/js/od-mesh.js +553 -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 +1107 -0
- package/src/superlocalmemory/ui/js/od-shell.js +809 -0
- package/src/superlocalmemory/ui/js/od-skills.js +600 -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 +17 -3
- 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/plugin-src/.mcp.json +0 -12
- package/plugin-src/agents/slm-memory-advisor.md +0 -44
- package/plugin-src/agents/slm-optimize-advisor.md +0 -38
- package/plugin-src/hooks/.gitkeep +0 -0
- package/plugin-src/hooks/hooks.json +0 -23
- package/plugin-src/manifest.json +0 -25
- package/plugin-src/requirements.txt +0 -1
- package/plugin-src/rules/CLAUDE.md.fragment +0 -44
- package/plugin-src/scripts/ensure-venv.bat +0 -122
- package/plugin-src/scripts/ensure-venv.sh +0 -105
- package/plugin-src/scripts/slm-launch +0 -23
- package/plugin-src/scripts/slm-launch.bat +0 -23
- package/plugin-src/settings.json +0 -16
- package/src/superlocalmemory/learning/behavioral_listener.py +0 -94
|
@@ -40,6 +40,14 @@ def _cmd_db_dispatch(args: Namespace) -> None:
|
|
|
40
40
|
sys.exit(2)
|
|
41
41
|
|
|
42
42
|
|
|
43
|
+
def _cmd_mesh_dispatch(args: Namespace) -> None:
|
|
44
|
+
"""Route ``slm mesh ...`` inspection subcommands (M-03)."""
|
|
45
|
+
from superlocalmemory.cli.mesh_cmd import cmd_mesh
|
|
46
|
+
rc = cmd_mesh(args)
|
|
47
|
+
if rc:
|
|
48
|
+
sys.exit(rc)
|
|
49
|
+
|
|
50
|
+
|
|
43
51
|
def _cmd_escape_disable(args: Namespace) -> None:
|
|
44
52
|
from superlocalmemory.cli.escape_hatch import cmd_disable
|
|
45
53
|
cmd_disable(args)
|
|
@@ -98,6 +106,11 @@ def _cmd_help_optimize(args: Namespace) -> None:
|
|
|
98
106
|
cmd_help_optimize(args)
|
|
99
107
|
|
|
100
108
|
|
|
109
|
+
def _cmd_loop(args: Namespace) -> None:
|
|
110
|
+
from superlocalmemory.cli.loop_cmd import cmd_loop
|
|
111
|
+
cmd_loop(args)
|
|
112
|
+
|
|
113
|
+
|
|
101
114
|
# ---- end SLM v3.6 Optimize dispatch functions ----
|
|
102
115
|
|
|
103
116
|
|
|
@@ -146,7 +159,10 @@ def cmd_session(args: Namespace) -> None:
|
|
|
146
159
|
def dispatch(args: Namespace) -> None:
|
|
147
160
|
"""Route CLI command to the appropriate handler."""
|
|
148
161
|
# Auto-install/upgrade hooks on version change (single file read, ~0.1ms)
|
|
149
|
-
if
|
|
162
|
+
if (
|
|
163
|
+
args.command not in ("hooks", "codex", "init", "mcp")
|
|
164
|
+
and not getattr(args, "dry_run", False)
|
|
165
|
+
):
|
|
150
166
|
try:
|
|
151
167
|
from superlocalmemory.hooks.claude_code_hooks import auto_install_if_needed
|
|
152
168
|
auto_install_if_needed()
|
|
@@ -201,6 +217,8 @@ def dispatch(args: Namespace) -> None:
|
|
|
201
217
|
"context": _cmd_context_dispatch,
|
|
202
218
|
# V3.4.22 LLD-06 additive schema migrations
|
|
203
219
|
"db": _cmd_db_dispatch,
|
|
220
|
+
# V3.7.9 M-03 — terminal mesh inspection
|
|
221
|
+
"mesh": _cmd_mesh_dispatch,
|
|
204
222
|
# V3.4.22 Stage 8 SB-5 — MASTER-PLAN §8 escape hatches.
|
|
205
223
|
"disable": _cmd_escape_disable,
|
|
206
224
|
"enable": _cmd_escape_enable,
|
|
@@ -218,6 +236,8 @@ def dispatch(args: Namespace) -> None:
|
|
|
218
236
|
"compress": _cmd_compress,
|
|
219
237
|
"proxy": _cmd_proxy,
|
|
220
238
|
"help-optimize": _cmd_help_optimize,
|
|
239
|
+
# V3.8.0 bounded loops (gate-verified agent loops, SLM-backed ledger)
|
|
240
|
+
"loop": _cmd_loop,
|
|
221
241
|
}
|
|
222
242
|
handler = handlers.get(args.command)
|
|
223
243
|
if handler:
|
|
@@ -345,6 +365,11 @@ def cmd_serve(args: Namespace) -> None:
|
|
|
345
365
|
else:
|
|
346
366
|
from superlocalmemory.infra.data_root import state_path
|
|
347
367
|
print(f"Failed to start daemon. Check {state_path('logs', 'daemon.log')}")
|
|
368
|
+
# INT-H-02: exit non-zero so callers can detect the failure. The plugin
|
|
369
|
+
# launcher (plugin/scripts/slm-launch) guards on this exit code and
|
|
370
|
+
# refuses to open a direct MCP writer against a broken daemon; without
|
|
371
|
+
# the non-zero exit that guard was a dead no-op.
|
|
372
|
+
sys.exit(1)
|
|
348
373
|
|
|
349
374
|
|
|
350
375
|
# -- Ingestion Adapters (V3.4.3) ------------------------------------------
|
|
@@ -610,6 +635,8 @@ def cmd_config(args: Namespace) -> None:
|
|
|
610
635
|
elif action == "set":
|
|
611
636
|
_ALLOWED_CONFIG_KEYS = {
|
|
612
637
|
"evolution.enabled", "evolution.backend", "evolution.max_evolutions_per_cycle",
|
|
638
|
+
"evolution.mutation_model", "evolution.verify_model",
|
|
639
|
+
"evolution.confirm_model",
|
|
613
640
|
"mesh_enabled", "daemon_idle_timeout", "entity_compilation_enabled",
|
|
614
641
|
"graph_backend", "vector_backend", "scale_engine_state",
|
|
615
642
|
"scope.default_scope", "scope.recall_include_global",
|
|
@@ -677,6 +704,31 @@ def cmd_config(args: Namespace) -> None:
|
|
|
677
704
|
print(f"Error: {message}")
|
|
678
705
|
sys.exit(1)
|
|
679
706
|
|
|
707
|
+
_MODEL_KEYS = {
|
|
708
|
+
"evolution.mutation_model", "evolution.verify_model",
|
|
709
|
+
"evolution.confirm_model",
|
|
710
|
+
}
|
|
711
|
+
if key in _MODEL_KEYS:
|
|
712
|
+
from superlocalmemory.evolution.model_selection import _MODEL_ALIASES
|
|
713
|
+
|
|
714
|
+
accepted = set(_MODEL_ALIASES) | {"", "auto"}
|
|
715
|
+
sval = str(parsed_value)
|
|
716
|
+
if sval not in accepted:
|
|
717
|
+
allowed = ", ".join(["auto", *sorted(_MODEL_ALIASES)])
|
|
718
|
+
message = f"{key} must be one of: {allowed}"
|
|
719
|
+
if use_json:
|
|
720
|
+
from superlocalmemory.cli.json_output import json_print
|
|
721
|
+
json_print("config", error={
|
|
722
|
+
"code": "INVALID_VALUE", "message": message,
|
|
723
|
+
})
|
|
724
|
+
else:
|
|
725
|
+
print(f"Error: {message}")
|
|
726
|
+
sys.exit(1)
|
|
727
|
+
# Normalise the "auto" sentinel to the empty string the
|
|
728
|
+
# resolver treats as "pick the cheapest for the backend".
|
|
729
|
+
if sval == "auto":
|
|
730
|
+
parsed_value = ""
|
|
731
|
+
|
|
680
732
|
if key.startswith("scope."):
|
|
681
733
|
from superlocalmemory.cli.daemon import daemon_request, is_daemon_running
|
|
682
734
|
|
|
@@ -727,6 +779,18 @@ def cmd_config(args: Namespace) -> None:
|
|
|
727
779
|
})
|
|
728
780
|
else:
|
|
729
781
|
print(f"{key}: {old_value} -> {parsed_value}")
|
|
782
|
+
if key == "evolution.enabled" and parsed_value is True:
|
|
783
|
+
print(
|
|
784
|
+
"\n⚠ Skill evolution is now ON. It makes background "
|
|
785
|
+
"LLM calls during consolidation\n"
|
|
786
|
+
" (capped at 10 calls/cycle, 3 cycles/day). It defaults "
|
|
787
|
+
"to the lowest-cost model\n"
|
|
788
|
+
" for your backend (Claude → Haiku, Ollama → "
|
|
789
|
+
"local/free).\n"
|
|
790
|
+
" Pick models: slm config set evolution.mutation_model "
|
|
791
|
+
"<auto|haiku|sonnet|ollama>\n"
|
|
792
|
+
" Turn off: slm config set evolution.enabled false"
|
|
793
|
+
)
|
|
730
794
|
|
|
731
795
|
else:
|
|
732
796
|
if use_json:
|
|
@@ -897,16 +961,22 @@ def _agents_md_source_factory():
|
|
|
897
961
|
Gracefully skips if absent — never fails the MCP write.
|
|
898
962
|
"""
|
|
899
963
|
from pathlib import Path
|
|
964
|
+
import sysconfig
|
|
900
965
|
|
|
901
966
|
# Resolve relative to the package root (src/superlocalmemory/../../)
|
|
902
967
|
_pkg_root = Path(__file__).resolve().parents[3]
|
|
903
|
-
|
|
968
|
+
_candidates = (
|
|
969
|
+
_pkg_root / "plugin-src" / "rules" / "AGENTS.md",
|
|
970
|
+
Path(sysconfig.get_path("data")) / "share" / "superlocalmemory"
|
|
971
|
+
/ "portable-kit" / "rules" / "AGENTS.md",
|
|
972
|
+
)
|
|
904
973
|
|
|
905
974
|
def _read() -> str | None:
|
|
906
|
-
|
|
907
|
-
|
|
975
|
+
for candidate in _candidates:
|
|
976
|
+
if candidate.exists():
|
|
977
|
+
return candidate.read_text(encoding="utf-8")
|
|
908
978
|
logger.warning(
|
|
909
|
-
"WP-05 AGENTS.md not
|
|
979
|
+
"WP-05 AGENTS.md is not bundled — skipping AGENTS.md write"
|
|
910
980
|
)
|
|
911
981
|
return None
|
|
912
982
|
|
|
@@ -947,9 +1017,10 @@ def cmd_connect(args: Namespace) -> None:
|
|
|
947
1017
|
here=here,
|
|
948
1018
|
profile=profile,
|
|
949
1019
|
agents_md_source=_agents_md_source_factory(),
|
|
1020
|
+
dry_run=getattr(args, "dry_run", False),
|
|
950
1021
|
)
|
|
951
1022
|
|
|
952
|
-
if not result.get("error"):
|
|
1023
|
+
if not result.get("error") and not getattr(args, "dry_run", False):
|
|
953
1024
|
from superlocalmemory.infra.local_diagnostics import record_operation
|
|
954
1025
|
|
|
955
1026
|
record_operation("activation", client=ide_arg)
|
|
@@ -968,6 +1039,7 @@ def cmd_connect(args: Namespace) -> None:
|
|
|
968
1039
|
sys.exit(1)
|
|
969
1040
|
|
|
970
1041
|
status_sym = {"wrote": "[+]", "merged": "[~]", "unchanged": "[=]",
|
|
1042
|
+
"would_write": "[~]",
|
|
971
1043
|
"skipped": "[s]", "error": "[!]"}.get(
|
|
972
1044
|
result["mcp_config"], "[?]"
|
|
973
1045
|
)
|
|
@@ -996,6 +1068,7 @@ def cmd_connect(args: Namespace) -> None:
|
|
|
996
1068
|
from superlocalmemory.hooks.ide_connector import IDEConnector
|
|
997
1069
|
|
|
998
1070
|
connector = IDEConnector()
|
|
1071
|
+
dry_run = bool(getattr(args, "dry_run", False))
|
|
999
1072
|
|
|
1000
1073
|
if getattr(args, 'json', False):
|
|
1001
1074
|
from superlocalmemory.cli.json_output import json_print
|
|
@@ -1007,6 +1080,14 @@ def cmd_connect(args: Namespace) -> None:
|
|
|
1007
1080
|
elif getattr(args, "ide", None):
|
|
1008
1081
|
success = connector.connect(args.ide)
|
|
1009
1082
|
json_print("connect", data={"ide": args.ide, "connected": success})
|
|
1083
|
+
elif dry_run:
|
|
1084
|
+
json_print(
|
|
1085
|
+
"connect",
|
|
1086
|
+
data={
|
|
1087
|
+
"dry_run": True,
|
|
1088
|
+
"would_configure": connector.get_status(),
|
|
1089
|
+
},
|
|
1090
|
+
)
|
|
1010
1091
|
else:
|
|
1011
1092
|
json_print("connect", data={"results": connector.connect_all()},
|
|
1012
1093
|
next_actions=[
|
|
@@ -1020,6 +1101,12 @@ def cmd_connect(args: Namespace) -> None:
|
|
|
1020
1101
|
mark = "[+]" if s["installed"] else "[-]"
|
|
1021
1102
|
print(f" {mark} {s['name']:20s} {s['config_path']}")
|
|
1022
1103
|
return
|
|
1104
|
+
if dry_run:
|
|
1105
|
+
status = connector.get_status()
|
|
1106
|
+
print("Dry run — no IDE, hook, config, or SLM data files were written.")
|
|
1107
|
+
for item in status:
|
|
1108
|
+
print(f" would inspect/configure: {item['name']} ({item['config_path']})")
|
|
1109
|
+
return
|
|
1023
1110
|
if getattr(args, "ide", None):
|
|
1024
1111
|
success = connector.connect(args.ide)
|
|
1025
1112
|
print(f"{'Connected' if success else 'Failed'}: {args.ide}")
|
|
@@ -1271,10 +1358,12 @@ def cmd_recall(args: Namespace) -> None:
|
|
|
1271
1358
|
scope_qs += f"&include_global={str(include_global).lower()}"
|
|
1272
1359
|
if include_shared is not None:
|
|
1273
1360
|
scope_qs += f"&include_shared={str(include_shared).lower()}"
|
|
1361
|
+
_window = getattr(args, "window", "") or ""
|
|
1362
|
+
window_qs = f"&window={quote(_window)}" if _window else ""
|
|
1274
1363
|
result = daemon_request(
|
|
1275
1364
|
"GET",
|
|
1276
1365
|
f"/recall?q={quote(args.query)}&limit={args.limit}"
|
|
1277
|
-
f"&session_id={quote(session_id)}{fast_qs}{scope_qs}",
|
|
1366
|
+
f"&session_id={quote(session_id)}{fast_qs}{scope_qs}{window_qs}",
|
|
1278
1367
|
)
|
|
1279
1368
|
if result and "results" in result:
|
|
1280
1369
|
# Format daemon response same as engine response
|
|
@@ -1294,8 +1383,10 @@ def cmd_recall(args: Namespace) -> None:
|
|
|
1294
1383
|
for i, r in enumerate(result["results"], 1):
|
|
1295
1384
|
print(f" {i}. [{r['score']:.2f}] {r['content']}")
|
|
1296
1385
|
return
|
|
1297
|
-
except Exception:
|
|
1298
|
-
|
|
1386
|
+
except Exception as _exc: # noqa: BLE001
|
|
1387
|
+
logger.warning(
|
|
1388
|
+
"Daemon recall failed, falling back to direct engine: %s", _exc
|
|
1389
|
+
)
|
|
1299
1390
|
|
|
1300
1391
|
from superlocalmemory.core.config import SLMConfig
|
|
1301
1392
|
from superlocalmemory.core.engine import MemoryEngine
|
|
@@ -1310,6 +1401,7 @@ def cmd_recall(args: Namespace) -> None:
|
|
|
1310
1401
|
fast=getattr(args, "fast", False),
|
|
1311
1402
|
include_global=include_global,
|
|
1312
1403
|
include_shared=include_shared,
|
|
1404
|
+
window=getattr(args, "window", "") or None,
|
|
1313
1405
|
)
|
|
1314
1406
|
except Exception as exc:
|
|
1315
1407
|
if use_json:
|
|
@@ -1394,12 +1486,32 @@ def cmd_forget(args: Namespace) -> None:
|
|
|
1394
1486
|
from superlocalmemory.core.config import SLMConfig
|
|
1395
1487
|
|
|
1396
1488
|
use_json = getattr(args, 'json', False)
|
|
1489
|
+
dry_run = getattr(args, 'dry_run', False)
|
|
1490
|
+
raw_query = getattr(args, 'query', None)
|
|
1491
|
+
|
|
1492
|
+
# F3: `query` is optional so `slm forget --dry-run` can preview every memory.
|
|
1493
|
+
# Deletion ALWAYS requires an explicit query — a bare `slm forget` must never
|
|
1494
|
+
# mass-delete. In dry-run mode a missing query means "match all" (preview).
|
|
1495
|
+
if raw_query is None and not dry_run:
|
|
1496
|
+
msg = (
|
|
1497
|
+
"A query is required to delete. Preview everything with "
|
|
1498
|
+
"'slm forget --dry-run', or delete matches with 'slm forget <query>'."
|
|
1499
|
+
)
|
|
1500
|
+
if use_json:
|
|
1501
|
+
from superlocalmemory.cli.json_output import json_print
|
|
1502
|
+
json_print("forget", error={"code": "QUERY_REQUIRED", "message": msg})
|
|
1503
|
+
else:
|
|
1504
|
+
print(msg)
|
|
1505
|
+
sys.exit(2)
|
|
1506
|
+
|
|
1507
|
+
query_lower = "" if raw_query is None else raw_query.lower()
|
|
1508
|
+
query_label = raw_query if raw_query is not None else "*all*"
|
|
1509
|
+
|
|
1397
1510
|
try:
|
|
1398
1511
|
config = SLMConfig.load()
|
|
1399
1512
|
engine = MemoryEngine(config)
|
|
1400
1513
|
engine.initialize()
|
|
1401
1514
|
facts = engine._db.get_all_facts(engine.profile_id)
|
|
1402
|
-
query_lower = args.query.lower()
|
|
1403
1515
|
matches = [f for f in facts if query_lower in f.content.lower()]
|
|
1404
1516
|
except Exception as exc:
|
|
1405
1517
|
if use_json:
|
|
@@ -1408,8 +1520,6 @@ def cmd_forget(args: Namespace) -> None:
|
|
|
1408
1520
|
sys.exit(1)
|
|
1409
1521
|
raise
|
|
1410
1522
|
|
|
1411
|
-
dry_run = getattr(args, 'dry_run', False)
|
|
1412
|
-
|
|
1413
1523
|
def delete_fact_authorized_for_cli(fact_id: str) -> None:
|
|
1414
1524
|
from superlocalmemory.core.engine_ingestion import local_trusted_actor_id
|
|
1415
1525
|
from superlocalmemory.core.mutations import delete_fact_authorized
|
|
@@ -1450,12 +1560,12 @@ def cmd_forget(args: Namespace) -> None:
|
|
|
1450
1560
|
"matches": match_items,
|
|
1451
1561
|
"hint": "Add --yes to confirm deletion",
|
|
1452
1562
|
}, next_actions=[
|
|
1453
|
-
{"command": f"slm forget '{
|
|
1563
|
+
{"command": f"slm forget '{query_label}' --json --yes", "description": "Confirm deletion"},
|
|
1454
1564
|
])
|
|
1455
1565
|
return
|
|
1456
1566
|
|
|
1457
1567
|
if not matches:
|
|
1458
|
-
print(f"No memories matching '{
|
|
1568
|
+
print(f"No memories matching '{query_label}'")
|
|
1459
1569
|
return
|
|
1460
1570
|
print(f"Found {len(matches)} matching memories:")
|
|
1461
1571
|
for f in matches[:10]:
|
|
@@ -1479,16 +1589,86 @@ def cmd_forget(args: Namespace) -> None:
|
|
|
1479
1589
|
|
|
1480
1590
|
def cmd_delete(args: Namespace) -> None:
|
|
1481
1591
|
"""Delete a specific memory by exact fact ID."""
|
|
1592
|
+
import urllib.parse
|
|
1593
|
+
|
|
1594
|
+
from superlocalmemory.cli.daemon import daemon_request, is_daemon_running
|
|
1482
1595
|
from superlocalmemory.core.config import SLMConfig
|
|
1483
1596
|
from superlocalmemory.core.engine import MemoryEngine
|
|
1484
1597
|
|
|
1485
1598
|
use_json = getattr(args, 'json', False)
|
|
1599
|
+
fact_id = args.fact_id.strip()
|
|
1600
|
+
if is_daemon_running():
|
|
1601
|
+
path = "/api/memories/" + urllib.parse.quote(fact_id, safe="")
|
|
1602
|
+
confirmed = getattr(args, "yes", False)
|
|
1603
|
+
content = ""
|
|
1604
|
+
if not confirmed:
|
|
1605
|
+
detail = daemon_request(
|
|
1606
|
+
"GET",
|
|
1607
|
+
"/api/facts/" + urllib.parse.quote(fact_id, safe=""),
|
|
1608
|
+
)
|
|
1609
|
+
if not isinstance(detail, dict):
|
|
1610
|
+
if use_json:
|
|
1611
|
+
from superlocalmemory.cli.json_output import json_print
|
|
1612
|
+
json_print("delete", error={
|
|
1613
|
+
"code": "DAEMON_MUTATION_FAILED",
|
|
1614
|
+
"message": "Resident daemon could not resolve the memory.",
|
|
1615
|
+
})
|
|
1616
|
+
sys.exit(1)
|
|
1617
|
+
raise RuntimeError("Resident daemon could not resolve the memory.")
|
|
1618
|
+
content = str(detail.get("content") or "")
|
|
1619
|
+
if use_json:
|
|
1620
|
+
from superlocalmemory.cli.json_output import json_print
|
|
1621
|
+
json_print("delete", data={
|
|
1622
|
+
"fact_id": fact_id,
|
|
1623
|
+
"content": content[:120],
|
|
1624
|
+
"deleted": False,
|
|
1625
|
+
"hint": "Add --yes to confirm deletion",
|
|
1626
|
+
}, next_actions=[
|
|
1627
|
+
{
|
|
1628
|
+
"command": f"slm delete {fact_id} --json --yes",
|
|
1629
|
+
"description": "Confirm deletion",
|
|
1630
|
+
},
|
|
1631
|
+
])
|
|
1632
|
+
return
|
|
1633
|
+
print(f"Memory: {content[:120]}")
|
|
1634
|
+
confirmed = input("Delete this memory? [y/N] ").strip().lower() in (
|
|
1635
|
+
"y", "yes",
|
|
1636
|
+
)
|
|
1637
|
+
if not confirmed:
|
|
1638
|
+
print("Cancelled.")
|
|
1639
|
+
return
|
|
1640
|
+
|
|
1641
|
+
result = daemon_request("DELETE", path)
|
|
1642
|
+
if not isinstance(result, dict) or not result.get("success"):
|
|
1643
|
+
if use_json:
|
|
1644
|
+
from superlocalmemory.cli.json_output import json_print
|
|
1645
|
+
json_print("delete", error={
|
|
1646
|
+
"code": "DAEMON_MUTATION_FAILED",
|
|
1647
|
+
"message": "Resident daemon rejected the delete operation.",
|
|
1648
|
+
})
|
|
1649
|
+
sys.exit(1)
|
|
1650
|
+
raise RuntimeError("Resident daemon rejected the delete operation.")
|
|
1651
|
+
if use_json:
|
|
1652
|
+
from superlocalmemory.cli.json_output import json_print
|
|
1653
|
+
json_print(
|
|
1654
|
+
"delete",
|
|
1655
|
+
data={"deleted": fact_id, "content": content[:120]},
|
|
1656
|
+
next_actions=[
|
|
1657
|
+
{
|
|
1658
|
+
"command": "slm list --json",
|
|
1659
|
+
"description": "Verify remaining memories",
|
|
1660
|
+
},
|
|
1661
|
+
],
|
|
1662
|
+
)
|
|
1663
|
+
else:
|
|
1664
|
+
print(f"Deleted: {fact_id}")
|
|
1665
|
+
return
|
|
1666
|
+
|
|
1486
1667
|
try:
|
|
1487
1668
|
config = SLMConfig.load()
|
|
1488
1669
|
engine = MemoryEngine(config)
|
|
1489
1670
|
engine.initialize()
|
|
1490
1671
|
|
|
1491
|
-
fact_id = args.fact_id.strip()
|
|
1492
1672
|
rows = engine._db.execute(
|
|
1493
1673
|
"SELECT content FROM atomic_facts WHERE fact_id = ? AND profile_id = ?",
|
|
1494
1674
|
(fact_id, engine.profile_id),
|
|
@@ -1558,6 +1738,9 @@ def cmd_delete(args: Namespace) -> None:
|
|
|
1558
1738
|
|
|
1559
1739
|
def cmd_update(args: Namespace) -> None:
|
|
1560
1740
|
"""Update the content of a specific memory by exact fact ID."""
|
|
1741
|
+
import urllib.parse
|
|
1742
|
+
|
|
1743
|
+
from superlocalmemory.cli.daemon import daemon_request, is_daemon_running
|
|
1561
1744
|
from superlocalmemory.core.config import SLMConfig
|
|
1562
1745
|
from superlocalmemory.core.engine import MemoryEngine
|
|
1563
1746
|
|
|
@@ -1573,6 +1756,34 @@ def cmd_update(args: Namespace) -> None:
|
|
|
1573
1756
|
print("Error: content cannot be empty")
|
|
1574
1757
|
return
|
|
1575
1758
|
|
|
1759
|
+
if is_daemon_running():
|
|
1760
|
+
path = "/api/memories/" + urllib.parse.quote(fact_id, safe="")
|
|
1761
|
+
result = daemon_request("PATCH", path, {"content": new_content})
|
|
1762
|
+
if not isinstance(result, dict) or not result.get("success"):
|
|
1763
|
+
if use_json:
|
|
1764
|
+
from superlocalmemory.cli.json_output import json_print
|
|
1765
|
+
json_print("update", error={
|
|
1766
|
+
"code": "DAEMON_MUTATION_FAILED",
|
|
1767
|
+
"message": "Resident daemon rejected the update operation.",
|
|
1768
|
+
})
|
|
1769
|
+
sys.exit(1)
|
|
1770
|
+
raise RuntimeError("Resident daemon rejected the update operation.")
|
|
1771
|
+
if use_json:
|
|
1772
|
+
from superlocalmemory.cli.json_output import json_print
|
|
1773
|
+
json_print("update", data={
|
|
1774
|
+
"fact_id": fact_id,
|
|
1775
|
+
"new_content": new_content[:120],
|
|
1776
|
+
}, next_actions=[
|
|
1777
|
+
{
|
|
1778
|
+
"command": "slm list --json",
|
|
1779
|
+
"description": "List recent memories",
|
|
1780
|
+
},
|
|
1781
|
+
])
|
|
1782
|
+
else:
|
|
1783
|
+
print(f"New: {new_content[:100]}")
|
|
1784
|
+
print(f"Updated: {fact_id}")
|
|
1785
|
+
return
|
|
1786
|
+
|
|
1576
1787
|
try:
|
|
1577
1788
|
config = SLMConfig.load()
|
|
1578
1789
|
engine = MemoryEngine(config)
|
|
@@ -1835,8 +2046,8 @@ def cmd_health(args: Namespace) -> None:
|
|
|
1835
2046
|
|
|
1836
2047
|
print("Math Layer Health:")
|
|
1837
2048
|
print(f" Total facts: {len(facts)}")
|
|
1838
|
-
print(f"
|
|
1839
|
-
print(f"
|
|
2049
|
+
print(f" Math layer indexed: {fisher_count}/{len(facts)}")
|
|
2050
|
+
print(f" Lifecycle positioned: {langevin_count}/{len(facts)}")
|
|
1840
2051
|
print(f" Mode: {config.mode.value.upper()}")
|
|
1841
2052
|
|
|
1842
2053
|
|
|
@@ -118,8 +118,13 @@ def cmd_compress_prose(args: Namespace) -> None:
|
|
|
118
118
|
cfg = store.get()
|
|
119
119
|
|
|
120
120
|
fields: dict = {"compress_prose": (value == "on")}
|
|
121
|
-
if value == "on"
|
|
122
|
-
|
|
121
|
+
if value == "on":
|
|
122
|
+
# Prose (Layer 2) only fires in aggressive mode, so turning it on sets a
|
|
123
|
+
# COHERENT state — it can never be left enabled-but-inert in safe mode.
|
|
124
|
+
if not cfg.compress_enabled:
|
|
125
|
+
fields["compress_enabled"] = True
|
|
126
|
+
if cfg.compress_mode != "aggressive":
|
|
127
|
+
fields["compress_mode"] = "aggressive"
|
|
123
128
|
|
|
124
129
|
try:
|
|
125
130
|
cfg = dataclasses.replace(cfg, **fields)
|
|
@@ -129,13 +134,18 @@ def cmd_compress_prose(args: Namespace) -> None:
|
|
|
129
134
|
sys.exit(1)
|
|
130
135
|
|
|
131
136
|
if use_json:
|
|
132
|
-
print(json.dumps({
|
|
137
|
+
print(json.dumps({
|
|
138
|
+
"status": "ok",
|
|
139
|
+
"compress_prose": value == "on",
|
|
140
|
+
"compress_mode": cfg.compress_mode,
|
|
141
|
+
}))
|
|
133
142
|
return
|
|
134
143
|
|
|
135
144
|
print(f"Prose compression (LLMLingua-2): {'ENABLED' if value == 'on' else 'DISABLED'}.")
|
|
136
145
|
if value == "on":
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
146
|
+
if "compress_mode" in fields:
|
|
147
|
+
print(" Compression mode set to 'aggressive' (required for Layer 2).")
|
|
148
|
+
print(" Requires the llmlingua package for lossy prose compression.")
|
|
149
|
+
if "compress_enabled" in fields:
|
|
150
|
+
print(" (also enabled global compression)")
|
|
141
151
|
print("Daemon hot-reload: active within 2s.")
|
|
@@ -267,6 +267,13 @@ def daemon_request(
|
|
|
267
267
|
if capability is not None:
|
|
268
268
|
headers["X-SLM-Daemon-Capability"] = capability
|
|
269
269
|
headers["X-SLM-Target-Instance"] = descriptor.instance_id
|
|
270
|
+
# Daemon ownership proves that this CLI targets the local instance; it
|
|
271
|
+
# does not replace a dashboard user's profile-scoped authorization in
|
|
272
|
+
# governed workspaces. The user opts in by supplying an explicit
|
|
273
|
+
# session through the process environment (never logged or persisted).
|
|
274
|
+
user_session = os.environ.get("SLM_USER_SESSION", "").strip()
|
|
275
|
+
if user_session:
|
|
276
|
+
headers["X-SLM-User-Session"] = user_session
|
|
270
277
|
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
|
271
278
|
resp = urllib.request.urlopen(req, timeout=timeout_seconds)
|
|
272
279
|
return json.loads(resp.read().decode())
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""``slm loop`` — inspect and demonstrate bounded loops backed by SLM memory.
|
|
2
|
+
|
|
3
|
+
Subcommands:
|
|
4
|
+
|
|
5
|
+
* ``slm loop demo`` run a built-in, keyless convergence demo (stub
|
|
6
|
+
proposer + deterministic gate) and persist every
|
|
7
|
+
lap to SLM memory. Proves the engine + ledger
|
|
8
|
+
end to end without a credentialed agent.
|
|
9
|
+
* ``slm loop history [--name]`` list recorded loop runs from SLM memory.
|
|
10
|
+
* ``slm loop show <run_id>`` show every lap of one run.
|
|
11
|
+
|
|
12
|
+
The durable value here is that a loop's history lives in the same SLM data
|
|
13
|
+
root as everything else the agent remembers — queryable via ``slm recall`` and
|
|
14
|
+
visible in the dashboard. Reads and the demo run through an in-process engine
|
|
15
|
+
store rooted at the active data root (``SLM_DATA_DIR`` or
|
|
16
|
+
``~/.superlocalmemory``); point ``SLM_DATA_DIR`` elsewhere to sandbox a run.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
from argparse import Namespace
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
from superlocalmemory.infra.data_root import canonical_data_root
|
|
27
|
+
from superlocalmemory.loops import (
|
|
28
|
+
Bounds,
|
|
29
|
+
LapResult,
|
|
30
|
+
SLMMemoryLedger,
|
|
31
|
+
Verdict,
|
|
32
|
+
open_engine_store,
|
|
33
|
+
run_bounded_loop,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _data_root() -> Path:
|
|
38
|
+
return canonical_data_root()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class _FailOpenLedger:
|
|
42
|
+
"""Wrap a ledger so a memory write hiccup never aborts a running loop.
|
|
43
|
+
|
|
44
|
+
A ledger is observability; the loop's correctness comes from the gate.
|
|
45
|
+
Write errors are counted and surfaced after the run rather than raised
|
|
46
|
+
mid-flight, so we never silently pretend they did not happen.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self, inner: Any) -> None:
|
|
50
|
+
self._inner = inner
|
|
51
|
+
self.write_errors: list[str] = []
|
|
52
|
+
|
|
53
|
+
def record(self, entry: Any) -> None:
|
|
54
|
+
try:
|
|
55
|
+
self._inner.record(entry)
|
|
56
|
+
except Exception as exc: # pragma: no cover - defensive path
|
|
57
|
+
self.write_errors.append(f"lap {getattr(entry, 'lap', '?')}: {exc}")
|
|
58
|
+
|
|
59
|
+
def laps(self, run_id: str) -> list:
|
|
60
|
+
return self._inner.laps(run_id)
|
|
61
|
+
|
|
62
|
+
def runs(self, name: str) -> list:
|
|
63
|
+
return self._inner.runs(name)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _open_ledger() -> tuple[SLMMemoryLedger, Any]:
|
|
67
|
+
store = open_engine_store(_data_root() / "memory.db")
|
|
68
|
+
return SLMMemoryLedger(store), store
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def cmd_loop(args: Namespace) -> None:
|
|
72
|
+
action = getattr(args, "loop_command", None)
|
|
73
|
+
if action == "demo":
|
|
74
|
+
_cmd_demo(args)
|
|
75
|
+
elif action == "history":
|
|
76
|
+
_cmd_history(args)
|
|
77
|
+
elif action == "show":
|
|
78
|
+
_cmd_show(args)
|
|
79
|
+
else:
|
|
80
|
+
print("Usage: slm loop {demo|history|show} [options]")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _cmd_demo(args: Namespace) -> None:
|
|
84
|
+
"""Run the convergence demo: the gate fails twice, then passes on lap 3."""
|
|
85
|
+
iterations = int(getattr(args, "iterations", 10) or 10)
|
|
86
|
+
as_json = bool(getattr(args, "json", False))
|
|
87
|
+
pass_on = 3
|
|
88
|
+
|
|
89
|
+
ledger, store = _open_ledger()
|
|
90
|
+
guarded = _FailOpenLedger(ledger)
|
|
91
|
+
try:
|
|
92
|
+
outcome = run_bounded_loop(
|
|
93
|
+
"convergence-demo",
|
|
94
|
+
bounds=Bounds(max_iterations=iterations),
|
|
95
|
+
runner=lambda lap: LapResult(changed=True, tokens=8),
|
|
96
|
+
gate=lambda lap: Verdict(lap >= pass_on, f"demo gate: lap {lap}"),
|
|
97
|
+
ledger=guarded,
|
|
98
|
+
)
|
|
99
|
+
laps = guarded.laps(outcome.run_id)
|
|
100
|
+
finally:
|
|
101
|
+
store.close()
|
|
102
|
+
|
|
103
|
+
if as_json:
|
|
104
|
+
print(json.dumps({
|
|
105
|
+
"status": outcome.status.value,
|
|
106
|
+
"reason": outcome.reason,
|
|
107
|
+
"laps": outcome.laps,
|
|
108
|
+
"run_id": outcome.run_id,
|
|
109
|
+
"ledger": [
|
|
110
|
+
{"lap": e.lap, "decision": e.decision, "passed": e.passed}
|
|
111
|
+
for e in laps
|
|
112
|
+
],
|
|
113
|
+
"write_errors": guarded.write_errors,
|
|
114
|
+
}, indent=2))
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
mark = "✓" if outcome.ok else "✗"
|
|
118
|
+
print(f"{mark} [{outcome.status.value}] {outcome.reason} (laps: {outcome.laps})")
|
|
119
|
+
for e in laps:
|
|
120
|
+
gate = "gate-pass" if e.passed else "gate-fail"
|
|
121
|
+
print(f" lap {e.lap}: {e.decision:<8} {gate} {e.detail}")
|
|
122
|
+
print(f"run_id: {outcome.run_id} (recall with tag loop:convergence-demo)")
|
|
123
|
+
if guarded.write_errors:
|
|
124
|
+
print(f"WARNING: {len(guarded.write_errors)} ledger write error(s): "
|
|
125
|
+
f"{guarded.write_errors[0]}")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _cmd_history(args: Namespace) -> None:
|
|
129
|
+
name = getattr(args, "name", None) or "convergence-demo"
|
|
130
|
+
as_json = bool(getattr(args, "json", False))
|
|
131
|
+
ledger, store = _open_ledger()
|
|
132
|
+
try:
|
|
133
|
+
run_ids = ledger.runs(name)
|
|
134
|
+
rows = []
|
|
135
|
+
for rid in run_ids:
|
|
136
|
+
laps = ledger.laps(rid)
|
|
137
|
+
last = laps[-1] if laps else None
|
|
138
|
+
rows.append({
|
|
139
|
+
"run_id": rid,
|
|
140
|
+
"laps": len(laps),
|
|
141
|
+
"final": last.decision if last else "unknown",
|
|
142
|
+
"ts": last.ts if last else "",
|
|
143
|
+
})
|
|
144
|
+
finally:
|
|
145
|
+
store.close()
|
|
146
|
+
|
|
147
|
+
if as_json:
|
|
148
|
+
print(json.dumps({"name": name, "runs": rows}, indent=2))
|
|
149
|
+
return
|
|
150
|
+
if not rows:
|
|
151
|
+
print(f"No recorded runs for loop '{name}'.")
|
|
152
|
+
return
|
|
153
|
+
print(f"Runs for loop '{name}':")
|
|
154
|
+
for r in rows:
|
|
155
|
+
print(f" {r['run_id']:<28} laps={r['laps']:<3} final={r['final']:<8} {r['ts']}")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _cmd_show(args: Namespace) -> None:
|
|
159
|
+
run_id = getattr(args, "run_id", None)
|
|
160
|
+
as_json = bool(getattr(args, "json", False))
|
|
161
|
+
if not run_id:
|
|
162
|
+
print("Usage: slm loop show <run_id>")
|
|
163
|
+
return
|
|
164
|
+
ledger, store = _open_ledger()
|
|
165
|
+
try:
|
|
166
|
+
laps = ledger.laps(run_id)
|
|
167
|
+
finally:
|
|
168
|
+
store.close()
|
|
169
|
+
|
|
170
|
+
if as_json:
|
|
171
|
+
print(json.dumps({
|
|
172
|
+
"run_id": run_id,
|
|
173
|
+
"laps": [
|
|
174
|
+
{"lap": e.lap, "ts": e.ts, "decision": e.decision,
|
|
175
|
+
"passed": e.passed, "detail": e.detail, "budget": e.budget}
|
|
176
|
+
for e in laps
|
|
177
|
+
],
|
|
178
|
+
}, indent=2))
|
|
179
|
+
return
|
|
180
|
+
if not laps:
|
|
181
|
+
print(f"No ledger entries for run '{run_id}'.")
|
|
182
|
+
return
|
|
183
|
+
print(f"Loop run {run_id} ({laps[0].name}):")
|
|
184
|
+
for e in laps:
|
|
185
|
+
gate = "gate-pass" if e.passed else "gate-fail"
|
|
186
|
+
print(f" lap {e.lap}: {e.decision:<8} {gate} {e.detail} "
|
|
187
|
+
f"[tokens={e.budget.get('tokens', 0)}]")
|