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
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
+
# Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
|
|
4
|
+
|
|
5
|
+
"""SLM v3.8.0 — MCP bounded-loop tools.
|
|
6
|
+
|
|
7
|
+
Exposes the ``superlocalmemory.loops`` engine over MCP so an agent connected
|
|
8
|
+
via MCP — not only the ``slm loop`` CLI or the ``/slm-loop`` command — can run
|
|
9
|
+
a gated, bounded loop and inspect its durable ledger. Bounded loops therefore
|
|
10
|
+
ship on three surfaces: CLI, plugin command, and MCP.
|
|
11
|
+
|
|
12
|
+
The loop's one invariant holds identically here: an INDEPENDENT gate decides
|
|
13
|
+
when the loop is done, never the agent's own claim. Over MCP the gate is an SLM
|
|
14
|
+
*recall*: the loop converges the first lap a memory matching ``gate_query``
|
|
15
|
+
becomes retrievable with confidence (the "verification lives in memory" model
|
|
16
|
+
the engine is designed around). That makes ``slm_loop_run`` a safe, shell-free
|
|
17
|
+
multi-agent coordination primitive — one agent waits, under strict bounds, for
|
|
18
|
+
a memory another agent will write into shared SLM.
|
|
19
|
+
|
|
20
|
+
Three tools:
|
|
21
|
+
* ``slm_loop_run`` — run one bounded, gate-verified loop to a terminal
|
|
22
|
+
outcome. Blocks (polling the gate) until the gate
|
|
23
|
+
passes or a bound trips. Every lap is persisted to
|
|
24
|
+
SLM memory (tag ``loop:<name>``) and shows on the
|
|
25
|
+
dashboard.
|
|
26
|
+
* ``slm_loop_history`` — list recorded runs for a loop name (read-only).
|
|
27
|
+
* ``slm_loop_show`` — show every lap of one run (read-only).
|
|
28
|
+
|
|
29
|
+
Fail-open: every tool body returns a dict; internal errors surface as
|
|
30
|
+
``ok: False`` with a message, never a raised exception.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import asyncio
|
|
36
|
+
import logging
|
|
37
|
+
import time
|
|
38
|
+
from typing import Any, Callable
|
|
39
|
+
|
|
40
|
+
from mcp.types import ToolAnnotations
|
|
41
|
+
|
|
42
|
+
from superlocalmemory.loops import (
|
|
43
|
+
Bounds,
|
|
44
|
+
LapResult,
|
|
45
|
+
Verdict,
|
|
46
|
+
engine_backed_ledger,
|
|
47
|
+
run_bounded_loop,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
logger = logging.getLogger("slm.mcp.tools_loops")
|
|
51
|
+
|
|
52
|
+
# ─── Exported tool name list (used by server.py + tests) ─────────────────────
|
|
53
|
+
|
|
54
|
+
_LOOP_TOOL_NAMES = (
|
|
55
|
+
"slm_loop_run",
|
|
56
|
+
"slm_loop_history",
|
|
57
|
+
"slm_loop_show",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
# ─── Hard caps (a loop tool must never hang the daemon or spin unbounded) ────
|
|
61
|
+
|
|
62
|
+
_MAX_ITERATIONS = 200
|
|
63
|
+
_MAX_WALLCLOCK_S = 120.0
|
|
64
|
+
_MIN_POLL_S = 0.25
|
|
65
|
+
_MAX_NAME_CHARS = 128
|
|
66
|
+
_MAX_QUERY_CHARS = 2000
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _top_score(resp: Any) -> float:
|
|
70
|
+
"""Highest result score in a RecallResponse (0.0 when there are none)."""
|
|
71
|
+
best = 0.0
|
|
72
|
+
for r in getattr(resp, "results", None) or []:
|
|
73
|
+
s = getattr(r, "score", None)
|
|
74
|
+
if s is None:
|
|
75
|
+
s = getattr(r, "relevance_score", 0.0) or 0.0
|
|
76
|
+
try:
|
|
77
|
+
best = max(best, float(s))
|
|
78
|
+
except (TypeError, ValueError):
|
|
79
|
+
continue
|
|
80
|
+
return best
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def register_loop_tools(server, get_engine: Callable) -> None:
|
|
84
|
+
"""Register the 3 bounded-loop tools on *server*.
|
|
85
|
+
|
|
86
|
+
*server* is duck-typed: must support the ``@server.tool()`` decorator.
|
|
87
|
+
Compatible with FastMCP, _FilteredServer, and the test mock server.
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
@server.tool(annotations=ToolAnnotations(readOnlyHint=False, destructiveHint=False))
|
|
91
|
+
async def slm_loop_run(
|
|
92
|
+
name: str,
|
|
93
|
+
gate_query: str,
|
|
94
|
+
gate_min_score: float = 0.0,
|
|
95
|
+
max_iterations: int = 20,
|
|
96
|
+
max_wallclock_s: float = 15.0,
|
|
97
|
+
poll_interval_s: float = 1.0,
|
|
98
|
+
max_tokens: int = 0,
|
|
99
|
+
no_progress_window: int = 0,
|
|
100
|
+
) -> dict:
|
|
101
|
+
"""Run one bounded loop that finishes only when an INDEPENDENT gate passes.
|
|
102
|
+
|
|
103
|
+
The gate is an SLM recall of ``gate_query``: the loop converges on the
|
|
104
|
+
first lap that recall returns a confident match scoring at least
|
|
105
|
+
``gate_min_score``. The agent cannot end the loop by asserting it is
|
|
106
|
+
done — only the gate can. The call BLOCKS (polling every
|
|
107
|
+
``poll_interval_s``) until the gate passes or a bound trips, then
|
|
108
|
+
returns the outcome. Every lap is written to SLM memory (tag
|
|
109
|
+
``loop:<name>``) and is visible on the dashboard.
|
|
110
|
+
|
|
111
|
+
Use it to wait, under strict bounds, for a verification or coordination
|
|
112
|
+
condition to hold — e.g. for another agent to record a "build passed"
|
|
113
|
+
memory in shared SLM.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
name: Loop name, also the memory tag. 1–128 chars.
|
|
117
|
+
gate_query: Recall query the independent gate checks each lap.
|
|
118
|
+
gate_min_score: Minimum top-result score to pass (0.0 = any
|
|
119
|
+
confident hit above the evidence floor).
|
|
120
|
+
max_iterations: Hard cap on laps (1–200).
|
|
121
|
+
max_wallclock_s: Hard cap on wall-clock seconds (0 disables; capped at 120).
|
|
122
|
+
poll_interval_s: Seconds to wait between laps (minimum 0.25).
|
|
123
|
+
max_tokens: Optional token budget (0 disables).
|
|
124
|
+
no_progress_window: Halt after this many consecutive no-change laps
|
|
125
|
+
(0 disables). A pure watcher never "changes", so leave at 0
|
|
126
|
+
unless the runner reports progress.
|
|
127
|
+
"""
|
|
128
|
+
try:
|
|
129
|
+
name = (name or "").strip()
|
|
130
|
+
if not name or len(name) > _MAX_NAME_CHARS:
|
|
131
|
+
return {"ok": False, "error": f"name must be 1–{_MAX_NAME_CHARS} chars"}
|
|
132
|
+
gate_query = (gate_query or "").strip()
|
|
133
|
+
if not gate_query or len(gate_query) > _MAX_QUERY_CHARS:
|
|
134
|
+
return {
|
|
135
|
+
"ok": False,
|
|
136
|
+
"error": f"gate_query must be 1–{_MAX_QUERY_CHARS} chars",
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
iters = max(1, min(int(max_iterations), _MAX_ITERATIONS))
|
|
140
|
+
wall = (
|
|
141
|
+
min(float(max_wallclock_s), _MAX_WALLCLOCK_S)
|
|
142
|
+
if max_wallclock_s and float(max_wallclock_s) > 0
|
|
143
|
+
else None
|
|
144
|
+
)
|
|
145
|
+
poll = max(float(poll_interval_s), _MIN_POLL_S)
|
|
146
|
+
tok = int(max_tokens) if max_tokens and int(max_tokens) > 0 else None
|
|
147
|
+
# 0 = disabled (a pure watcher never "changes", so no-progress must
|
|
148
|
+
# be off by default or it would halt before the gate can pass).
|
|
149
|
+
npw = (
|
|
150
|
+
int(no_progress_window)
|
|
151
|
+
if no_progress_window and int(no_progress_window) > 0
|
|
152
|
+
else 0
|
|
153
|
+
)
|
|
154
|
+
min_score = float(gate_min_score)
|
|
155
|
+
|
|
156
|
+
engine = get_engine()
|
|
157
|
+
|
|
158
|
+
def gate(lap: int) -> Verdict:
|
|
159
|
+
resp = engine.recall(gate_query, limit=3, fast=True)
|
|
160
|
+
results = getattr(resp, "results", None) or []
|
|
161
|
+
floored = bool(getattr(resp, "no_confident_match", False))
|
|
162
|
+
top = _top_score(resp)
|
|
163
|
+
passed = bool(results) and not floored and top >= min_score
|
|
164
|
+
return Verdict(
|
|
165
|
+
passed,
|
|
166
|
+
f"recall '{gate_query[:48]}': hits={len(results)} "
|
|
167
|
+
f"top={top:.3f} floor={floored}",
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
def runner(lap: int) -> LapResult:
|
|
171
|
+
# Watcher lap: no work of our own. Give the gate condition time
|
|
172
|
+
# to become true (e.g. another agent writing a memory) between
|
|
173
|
+
# polls. Bounded by poll interval so the wall-clock / iteration
|
|
174
|
+
# caps stay meaningful. The first lap checks immediately.
|
|
175
|
+
if lap > 1:
|
|
176
|
+
time.sleep(poll)
|
|
177
|
+
return LapResult(changed=False, tokens=0)
|
|
178
|
+
|
|
179
|
+
ledger = engine_backed_ledger(engine)
|
|
180
|
+
|
|
181
|
+
# The loop blocks (sleeps between laps); run it off the event loop.
|
|
182
|
+
# The engine's per-call WAL connection model makes this thread-safe.
|
|
183
|
+
outcome = await asyncio.to_thread(
|
|
184
|
+
run_bounded_loop,
|
|
185
|
+
name,
|
|
186
|
+
bounds=Bounds(
|
|
187
|
+
max_iterations=iters,
|
|
188
|
+
max_tokens=tok,
|
|
189
|
+
max_wallclock_s=wall,
|
|
190
|
+
no_progress_window=npw,
|
|
191
|
+
),
|
|
192
|
+
runner=runner,
|
|
193
|
+
gate=gate,
|
|
194
|
+
ledger=ledger,
|
|
195
|
+
)
|
|
196
|
+
laps = await asyncio.to_thread(ledger.laps, outcome.run_id)
|
|
197
|
+
return {
|
|
198
|
+
"ok": True,
|
|
199
|
+
"status": outcome.status.value,
|
|
200
|
+
"reason": outcome.reason,
|
|
201
|
+
"passed": bool(outcome.ok),
|
|
202
|
+
"laps": outcome.laps,
|
|
203
|
+
"run_id": outcome.run_id,
|
|
204
|
+
"ledger": [
|
|
205
|
+
{
|
|
206
|
+
"lap": e.lap,
|
|
207
|
+
"decision": e.decision,
|
|
208
|
+
"passed": e.passed,
|
|
209
|
+
"detail": e.detail,
|
|
210
|
+
}
|
|
211
|
+
for e in laps
|
|
212
|
+
],
|
|
213
|
+
"note": (
|
|
214
|
+
"The gate — an independent SLM recall — decided this "
|
|
215
|
+
"outcome; the agent's own done-claim never terminates a loop."
|
|
216
|
+
),
|
|
217
|
+
}
|
|
218
|
+
except Exception as exc:
|
|
219
|
+
logger.exception("slm_loop_run failed (fail-open)")
|
|
220
|
+
return {"ok": False, "error": str(exc)}
|
|
221
|
+
|
|
222
|
+
@server.tool(annotations=ToolAnnotations(readOnlyHint=True))
|
|
223
|
+
async def slm_loop_history(name: str, limit: int = 20) -> dict:
|
|
224
|
+
"""List recorded bounded-loop runs for a loop name, newest laps summarised.
|
|
225
|
+
|
|
226
|
+
Reads the durable SLM-backed ledger (the same rows ``slm loop history``
|
|
227
|
+
and the dashboard show). Read-only.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
name: Loop name to list runs for.
|
|
231
|
+
limit: Maximum runs to return (1–200).
|
|
232
|
+
"""
|
|
233
|
+
try:
|
|
234
|
+
name = (name or "").strip()
|
|
235
|
+
if not name:
|
|
236
|
+
return {"ok": False, "error": "name is required"}
|
|
237
|
+
lim = max(1, min(int(limit), 200))
|
|
238
|
+
engine = get_engine()
|
|
239
|
+
ledger = engine_backed_ledger(engine)
|
|
240
|
+
|
|
241
|
+
def _collect() -> list[dict]:
|
|
242
|
+
run_ids = ledger.runs(name)[:lim]
|
|
243
|
+
rows: list[dict] = []
|
|
244
|
+
for rid in run_ids:
|
|
245
|
+
laps = ledger.laps(rid)
|
|
246
|
+
last = laps[-1] if laps else None
|
|
247
|
+
rows.append(
|
|
248
|
+
{
|
|
249
|
+
"run_id": rid,
|
|
250
|
+
"laps": len(laps),
|
|
251
|
+
"final": last.decision if last else "unknown",
|
|
252
|
+
"ts": last.ts if last else "",
|
|
253
|
+
}
|
|
254
|
+
)
|
|
255
|
+
return rows
|
|
256
|
+
|
|
257
|
+
rows = await asyncio.to_thread(_collect)
|
|
258
|
+
return {"ok": True, "name": name, "count": len(rows), "runs": rows}
|
|
259
|
+
except Exception as exc:
|
|
260
|
+
logger.exception("slm_loop_history failed (fail-open)")
|
|
261
|
+
return {"ok": False, "error": str(exc)}
|
|
262
|
+
|
|
263
|
+
@server.tool(annotations=ToolAnnotations(readOnlyHint=True))
|
|
264
|
+
async def slm_loop_show(run_id: str, limit: int = 200) -> dict:
|
|
265
|
+
"""Show every lap of one bounded-loop run, in order, from SLM memory.
|
|
266
|
+
|
|
267
|
+
Read-only. Each lap carries the gate verdict plus the agent's recorded
|
|
268
|
+
(advisory, never loop-terminating) done-claim.
|
|
269
|
+
|
|
270
|
+
Args:
|
|
271
|
+
run_id: Run identifier returned by ``slm_loop_run``.
|
|
272
|
+
limit: Maximum laps to return (1–1000).
|
|
273
|
+
"""
|
|
274
|
+
try:
|
|
275
|
+
run_id = (run_id or "").strip()
|
|
276
|
+
if not run_id:
|
|
277
|
+
return {"ok": False, "error": "run_id is required"}
|
|
278
|
+
lim = max(1, min(int(limit), 1000))
|
|
279
|
+
engine = get_engine()
|
|
280
|
+
ledger = engine_backed_ledger(engine)
|
|
281
|
+
|
|
282
|
+
def _collect() -> list[dict]:
|
|
283
|
+
return [
|
|
284
|
+
{
|
|
285
|
+
"lap": e.lap,
|
|
286
|
+
"ts": e.ts,
|
|
287
|
+
"decision": e.decision,
|
|
288
|
+
"passed": e.passed,
|
|
289
|
+
"detail": e.detail,
|
|
290
|
+
"agent_claimed_done": e.agent_claimed_done,
|
|
291
|
+
"tokens": e.budget.get("tokens", 0),
|
|
292
|
+
}
|
|
293
|
+
for e in ledger.laps(run_id)[:lim]
|
|
294
|
+
]
|
|
295
|
+
|
|
296
|
+
laps = await asyncio.to_thread(_collect)
|
|
297
|
+
return {"ok": True, "run_id": run_id, "count": len(laps), "laps": laps}
|
|
298
|
+
except Exception as exc:
|
|
299
|
+
logger.exception("slm_loop_show failed (fail-open)")
|
|
300
|
+
return {"ok": False, "error": str(exc)}
|
|
@@ -28,6 +28,103 @@ from mcp.types import ToolAnnotations
|
|
|
28
28
|
|
|
29
29
|
logger = logging.getLogger(__name__)
|
|
30
30
|
|
|
31
|
+
|
|
32
|
+
# ---------------------------------------------------------------------------
|
|
33
|
+
# M03: Simple circuit breaker for mesh_send
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
# Prevents runaway retries (~30s stall) when the daemon is dead.
|
|
36
|
+
# State machine: CLOSED → OPEN (after 3 consecutive failures)
|
|
37
|
+
# OPEN → HALF_OPEN (after 60s cooldown)
|
|
38
|
+
# HALF_OPEN → CLOSED (on one successful probe)
|
|
39
|
+
# HALF_OPEN → OPEN (on probe failure)
|
|
40
|
+
|
|
41
|
+
_CB_FAILURE_THRESHOLD = 3
|
|
42
|
+
_CB_COOLDOWN_SECONDS = 60
|
|
43
|
+
|
|
44
|
+
# Client-side mesh message cap (mirrors the broker's MAX_MESSAGE_SIZE).
|
|
45
|
+
MAX_MESSAGE_SIZE = 4096
|
|
46
|
+
|
|
47
|
+
_CB_STATE_CLOSED = "closed"
|
|
48
|
+
_CB_STATE_OPEN = "open"
|
|
49
|
+
_CB_STATE_HALF_OPEN = "half_open"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class _SendCircuitBreaker:
|
|
53
|
+
"""Thread-safe circuit breaker scoped to mesh_send daemon calls."""
|
|
54
|
+
|
|
55
|
+
def __init__(self) -> None:
|
|
56
|
+
self._lock = threading.Lock()
|
|
57
|
+
self._state: str = _CB_STATE_CLOSED
|
|
58
|
+
self._failure_count: int = 0
|
|
59
|
+
self._opened_at: float = 0.0
|
|
60
|
+
|
|
61
|
+
def reset(self) -> None:
|
|
62
|
+
"""Reset to CLOSED; used by tests for isolation."""
|
|
63
|
+
with self._lock:
|
|
64
|
+
self._state = _CB_STATE_CLOSED
|
|
65
|
+
self._failure_count = 0
|
|
66
|
+
self._opened_at = 0.0
|
|
67
|
+
|
|
68
|
+
def is_open(self) -> bool:
|
|
69
|
+
with self._lock:
|
|
70
|
+
if self._state == _CB_STATE_OPEN:
|
|
71
|
+
if time.monotonic() - self._opened_at >= _CB_COOLDOWN_SECONDS:
|
|
72
|
+
self._state = _CB_STATE_HALF_OPEN
|
|
73
|
+
return False # allow one probe
|
|
74
|
+
return True
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
def allow_request(self) -> bool:
|
|
78
|
+
"""Return True if the call should proceed; False if circuit is OPEN.
|
|
79
|
+
|
|
80
|
+
State transitions inside the lock prevent concurrent probe races:
|
|
81
|
+
- CLOSED → allow
|
|
82
|
+
- OPEN (cooldown not elapsed) → block
|
|
83
|
+
- OPEN (cooldown elapsed) → transition to HALF_OPEN, allow one probe,
|
|
84
|
+
and immediately re-enter OPEN so any concurrent call is blocked until
|
|
85
|
+
the probe result comes in via record_success / record_failure.
|
|
86
|
+
- HALF_OPEN → block (probe already dispatched and not yet resolved)
|
|
87
|
+
"""
|
|
88
|
+
with self._lock:
|
|
89
|
+
if self._state == _CB_STATE_CLOSED:
|
|
90
|
+
return True
|
|
91
|
+
if self._state == _CB_STATE_OPEN:
|
|
92
|
+
if time.monotonic() - self._opened_at >= _CB_COOLDOWN_SECONDS:
|
|
93
|
+
# Grant exactly one probe by briefly entering HALF_OPEN then
|
|
94
|
+
# going back to OPEN. Subsequent callers are blocked until
|
|
95
|
+
# record_success or record_failure resolves the probe.
|
|
96
|
+
self._state = _CB_STATE_HALF_OPEN
|
|
97
|
+
return True
|
|
98
|
+
return False
|
|
99
|
+
# HALF_OPEN: probe is in flight — block until resolved
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
def record_success(self) -> None:
|
|
103
|
+
with self._lock:
|
|
104
|
+
self._state = _CB_STATE_CLOSED
|
|
105
|
+
self._failure_count = 0
|
|
106
|
+
|
|
107
|
+
def record_failure(self) -> None:
|
|
108
|
+
with self._lock:
|
|
109
|
+
if self._state == _CB_STATE_HALF_OPEN:
|
|
110
|
+
# Probe failed — reopen immediately
|
|
111
|
+
self._state = _CB_STATE_OPEN
|
|
112
|
+
self._opened_at = time.monotonic()
|
|
113
|
+
return
|
|
114
|
+
self._failure_count += 1
|
|
115
|
+
if self._failure_count >= _CB_FAILURE_THRESHOLD:
|
|
116
|
+
self._state = _CB_STATE_OPEN
|
|
117
|
+
self._opened_at = time.monotonic()
|
|
118
|
+
logger.warning(
|
|
119
|
+
"mesh_send circuit breaker OPEN after %d consecutive failures; "
|
|
120
|
+
"fast-failing for %ds",
|
|
121
|
+
self._failure_count,
|
|
122
|
+
_CB_COOLDOWN_SECONDS,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
_SEND_CIRCUIT = _SendCircuitBreaker()
|
|
127
|
+
|
|
31
128
|
# Unique peer ID for this MCP server session
|
|
32
129
|
_PEER_ID = str(uuid.uuid4())[:12]
|
|
33
130
|
_SESSION_SUMMARY = ""
|
|
@@ -69,7 +166,15 @@ def _ensure_registered() -> None:
|
|
|
69
166
|
"session_id": os.environ.get("CLAUDE_SESSION_ID", _PEER_ID),
|
|
70
167
|
"summary": _SESSION_SUMMARY or "SLM MCP session",
|
|
71
168
|
"project_path": _PROJECT_PATH,
|
|
72
|
-
|
|
169
|
+
# Peer identity is the canonical SLM_AGENT_ID (same var memory
|
|
170
|
+
# attribution uses), so Antigravity/Hermes/Cursor/etc. show as
|
|
171
|
+
# themselves instead of collapsing to "claude_code". Fall back to the
|
|
172
|
+
# legacy CLAUDE_AGENT_TYPE, then a generic default.
|
|
173
|
+
"agent_type": (
|
|
174
|
+
os.environ.get("SLM_AGENT_ID")
|
|
175
|
+
or os.environ.get("CLAUDE_AGENT_TYPE")
|
|
176
|
+
or "claude_code"
|
|
177
|
+
),
|
|
73
178
|
})
|
|
74
179
|
if result:
|
|
75
180
|
# v3.6.12 (mesh-1): the broker mints its OWN peer_id (RegisterRequest has
|
|
@@ -174,12 +279,34 @@ def register_mesh_tools(server, get_engine: Callable) -> None:
|
|
|
174
279
|
- "project:/path/to/dir" (all sessions in that project directory)
|
|
175
280
|
message: The message content (max 4KB — use file paths for large data)
|
|
176
281
|
"""
|
|
282
|
+
# Enforce the documented 4KB notification cap client-side too (the
|
|
283
|
+
# broker also caps, but fail fast without a round-trip).
|
|
284
|
+
if len(message.encode("utf-8")) > MAX_MESSAGE_SIZE:
|
|
285
|
+
return {"ok": False, "error": (
|
|
286
|
+
f"message too large (max {MAX_MESSAGE_SIZE} bytes) — "
|
|
287
|
+
"reference a file path instead")}
|
|
288
|
+
# M03: circuit breaker — fast-fail if daemon is repeatedly unreachable
|
|
289
|
+
if not _SEND_CIRCUIT.allow_request():
|
|
290
|
+
return {
|
|
291
|
+
"ok": False,
|
|
292
|
+
"error": (
|
|
293
|
+
"mesh_send circuit open: daemon unreachable after repeated failures. "
|
|
294
|
+
f"Retrying in {_CB_COOLDOWN_SECONDS}s."
|
|
295
|
+
),
|
|
296
|
+
}
|
|
297
|
+
|
|
177
298
|
await asyncio.to_thread(_ensure_registered)
|
|
178
299
|
result = await asyncio.to_thread(
|
|
179
300
|
_mesh_request, "POST", "/send",
|
|
180
301
|
{"from_peer": _PEER_ID, "to_peer": to, "content": message},
|
|
181
302
|
)
|
|
182
|
-
|
|
303
|
+
# Circuit tracks daemon-unreachable (None) only — not valid broker errors
|
|
304
|
+
# like "recipient not found", which are application-level, not failures.
|
|
305
|
+
if result is None:
|
|
306
|
+
_SEND_CIRCUIT.record_failure()
|
|
307
|
+
return {"ok": False, "error": "Failed to send message"}
|
|
308
|
+
_SEND_CIRCUIT.record_success()
|
|
309
|
+
return result
|
|
183
310
|
|
|
184
311
|
@server.tool()
|
|
185
312
|
async def mesh_inbox() -> dict:
|
|
@@ -190,9 +317,11 @@ def register_mesh_tools(server, get_engine: Callable) -> None:
|
|
|
190
317
|
Messages auto-expire after 48 hours.
|
|
191
318
|
"""
|
|
192
319
|
await asyncio.to_thread(_ensure_registered)
|
|
320
|
+
from urllib.parse import quote
|
|
193
321
|
project = _PROJECT_PATH or _detect_project_path()
|
|
194
322
|
messages = await asyncio.to_thread(
|
|
195
|
-
_mesh_request, "GET",
|
|
323
|
+
_mesh_request, "GET",
|
|
324
|
+
f"/inbox/{_PEER_ID}?project_path={quote(project, safe='')}",
|
|
196
325
|
)
|
|
197
326
|
msg_list = (messages or {}).get("messages", [])
|
|
198
327
|
# Auto-mark unread messages as read. v3.6.12 (failopen-2): use .get("id")
|
|
@@ -250,9 +379,16 @@ def register_mesh_tools(server, get_engine: Callable) -> None:
|
|
|
250
379
|
Before editing a shared file, check if another session has it locked.
|
|
251
380
|
|
|
252
381
|
Args:
|
|
253
|
-
file_path:
|
|
382
|
+
file_path: Absolute path to the file
|
|
254
383
|
action: "query" (check lock), "acquire" (lock file), "release" (unlock)
|
|
255
384
|
"""
|
|
385
|
+
# Require a non-empty absolute path; a relative/blank path is ambiguous
|
|
386
|
+
# and lets a caller probe arbitrary strings via the coordination store.
|
|
387
|
+
if not file_path or not (
|
|
388
|
+
file_path.startswith("/")
|
|
389
|
+
or (len(file_path) >= 3 and file_path[1] == ":")
|
|
390
|
+
):
|
|
391
|
+
return {"ok": False, "error": "file_path must be a non-empty absolute path"}
|
|
256
392
|
await asyncio.to_thread(_ensure_registered)
|
|
257
393
|
result = await asyncio.to_thread(
|
|
258
394
|
_mesh_request, "POST", "/lock",
|
|
@@ -170,7 +170,7 @@ def register_optimize_tools(server) -> None:
|
|
|
170
170
|
"ok": False, "content": None, "size_bytes": 0,
|
|
171
171
|
"error": "ccr_id must be a UUID4",
|
|
172
172
|
}
|
|
173
|
-
original = CCRStore.get_instance().retrieve(ccr_id)
|
|
173
|
+
original = CCRStore.get_instance().retrieve(ccr_id, tenant_id=_tenant())
|
|
174
174
|
if original is None:
|
|
175
175
|
return {
|
|
176
176
|
"ok": False, "content": None, "size_bytes": 0,
|
|
@@ -257,13 +257,16 @@ def register_optimize_tools(server) -> None:
|
|
|
257
257
|
cache_key = hashlib.sha256(f"mcpkv:{tenant}:{key}".encode()).hexdigest()
|
|
258
258
|
norm_tid = _normalize_tenant_id(tenant)
|
|
259
259
|
|
|
260
|
-
|
|
260
|
+
db = CacheDB.get_default()
|
|
261
|
+
blob = db.get_value(cache_key, norm_tid)
|
|
261
262
|
if blob is None:
|
|
262
263
|
with _kv_lock:
|
|
263
264
|
_kv_misses += 1
|
|
265
|
+
db.kv_counter_incr("kv_misses") # M2: durable across restarts
|
|
264
266
|
return {"ok": True, "hit": False, "value": None, "note": None}
|
|
265
267
|
with _kv_lock:
|
|
266
268
|
_kv_hits += 1
|
|
269
|
+
db.kv_counter_incr("kv_hits") # M2: durable across restarts
|
|
267
270
|
return {"ok": True, "hit": True, "value": blob.decode("utf-8"), "note": None}
|
|
268
271
|
|
|
269
272
|
except Exception as exc:
|
|
@@ -277,14 +280,18 @@ def register_optimize_tools(server) -> None:
|
|
|
277
280
|
async def slm_optimize_stats() -> dict:
|
|
278
281
|
"""Return compression and cache statistics.
|
|
279
282
|
|
|
280
|
-
Proxy/compress stats are daemon-persisted (accurate across
|
|
281
|
-
KV
|
|
283
|
+
Proxy/compress AND KV stats are daemon-persisted (accurate across
|
|
284
|
+
restarts). KV counters fall back to this session's in-memory tally if
|
|
285
|
+
the persisted counters can't be read.
|
|
282
286
|
"""
|
|
283
287
|
try:
|
|
284
|
-
|
|
288
|
+
db = CacheDB.get_default()
|
|
289
|
+
snap = db.metrics_load()
|
|
290
|
+
# M2: prefer the durable KV counters; fall back to session counters.
|
|
291
|
+
persisted = db.kv_counters_load()
|
|
285
292
|
with _kv_lock:
|
|
286
|
-
kv_h = _kv_hits
|
|
287
|
-
kv_m = _kv_misses
|
|
293
|
+
kv_h = persisted.get("kv_hits", _kv_hits)
|
|
294
|
+
kv_m = persisted.get("kv_misses", _kv_misses)
|
|
288
295
|
return {
|
|
289
296
|
"ok": True,
|
|
290
297
|
"compress_runs": snap.compress_runs,
|
|
@@ -297,7 +304,7 @@ def register_optimize_tools(server) -> None:
|
|
|
297
304
|
"CCR entry count not tracked per-session; "
|
|
298
305
|
"see daemon /api/v1/metrics"
|
|
299
306
|
),
|
|
300
|
-
"note": "proxy stats are daemon-persisted
|
|
307
|
+
"note": "proxy and kv stats are daemon-persisted across restarts",
|
|
301
308
|
}
|
|
302
309
|
except Exception as exc:
|
|
303
310
|
logger.error("slm_optimize_stats failed (fail-open): %s", exc)
|