superlocalmemory 3.8.13 → 4.0.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/ATTRIBUTION.md +4 -4
- package/CHANGELOG.md +113 -121
- package/README.md +65 -63
- package/docs/pi-dev-integration.md +1 -1
- package/package.json +6 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/CLAUDE.md +3 -3
- package/plugin/agents/slm-governance-advisor.md +1 -1
- package/plugin/agents/slm-loop-runner.md +1 -1
- package/plugin/agents/slm-memory-advisor.md +1 -1
- package/plugin/agents/slm-optimize-advisor.md +1 -1
- package/plugin/requirements.txt +1 -1
- package/plugin/skills/slm-cache/SKILL.md +1 -1
- package/plugin/skills/slm-compress/SKILL.md +1 -1
- package/plugin/skills/slm-governance/SKILL.md +1 -1
- package/plugin/skills/slm-graph/SKILL.md +1 -1
- package/plugin/skills/slm-loop/SKILL.md +1 -1
- package/plugin/skills/slm-mesh/SKILL.md +1 -1
- package/plugin/skills/slm-profile/SKILL.md +1 -1
- package/plugin/skills/slm-recall/SKILL.md +1 -1
- package/plugin/skills/slm-remember/SKILL.md +1 -1
- package/plugin/skills/slm-scope/SKILL.md +1 -1
- package/plugin/skills/slm-session/SKILL.md +1 -1
- package/plugin/skills/slm-status/SKILL.md +1 -1
- package/plugin-src/rules/AGENTS.md +1 -1
- package/plugin-src/skills/slm-cache/SKILL.md +1 -1
- package/plugin-src/skills/slm-compress/SKILL.md +1 -1
- package/plugin-src/skills/slm-governance/SKILL.md +248 -0
- package/plugin-src/skills/slm-graph/SKILL.md +1 -1
- 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 +1 -1
- package/plugin-src/skills/slm-remember/SKILL.md +1 -1
- package/plugin-src/skills/slm-scope/SKILL.md +176 -0
- package/plugin-src/skills/slm-session/SKILL.md +1 -1
- package/plugin-src/skills/slm-status/SKILL.md +1 -1
- package/pyproject.toml +11 -4
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/commands.py +125 -11
- package/src/superlocalmemory/cli/daemon.py +5 -1
- package/src/superlocalmemory/cli/main.py +35 -2
- package/src/superlocalmemory/cli/ops_cmd.py +281 -0
- package/src/superlocalmemory/cli/setup_wizard.py +1 -1
- package/src/superlocalmemory/compliance/audit.py +65 -0
- package/src/superlocalmemory/compliance/eu_ai_act.py +27 -57
- package/src/superlocalmemory/compliance/gdpr.py +416 -20
- package/src/superlocalmemory/compliance/retention.py +74 -22
- package/src/superlocalmemory/compliance/scheduler.py +78 -9
- package/src/superlocalmemory/core/actor_context.py +166 -0
- package/src/superlocalmemory/core/admission.py +549 -0
- package/src/superlocalmemory/core/backend_orchestrator.py +23 -10
- package/src/superlocalmemory/core/config.py +202 -24
- package/src/superlocalmemory/core/consolidation_engine.py +13 -13
- package/src/superlocalmemory/core/context_cache.py +28 -0
- package/src/superlocalmemory/core/embeddings.py +64 -2
- package/src/superlocalmemory/core/engine.py +7 -2
- package/src/superlocalmemory/core/engine_ingestion.py +65 -3
- package/src/superlocalmemory/core/engine_wiring.py +36 -9
- package/src/superlocalmemory/core/ingest_policy.py +38 -0
- package/src/superlocalmemory/core/maintenance.py +255 -0
- package/src/superlocalmemory/core/modes.py +40 -13
- package/src/superlocalmemory/core/mutations.py +437 -44
- package/src/superlocalmemory/core/operation_policy.py +92 -0
- package/src/superlocalmemory/core/operation_policy_registry.py +542 -0
- package/src/superlocalmemory/core/operation_request.py +127 -0
- package/src/superlocalmemory/core/ops_remediation.py +542 -0
- package/src/superlocalmemory/core/recall_pipeline.py +7 -0
- package/src/superlocalmemory/core/remember_runtime.py +202 -4
- package/src/superlocalmemory/core/remote_mode.py +20 -5
- package/src/superlocalmemory/core/store_pipeline.py +150 -0
- package/src/superlocalmemory/core/topic_signature.py +19 -4
- package/src/superlocalmemory/core/transactions/__init__.py +78 -0
- package/src/superlocalmemory/core/transactions/concrete_owners.py +597 -0
- package/src/superlocalmemory/core/transactions/erasure.py +825 -0
- package/src/superlocalmemory/core/transactions/manifest.py +255 -0
- package/src/superlocalmemory/core/transactions/manifest_key.py +155 -0
- package/src/superlocalmemory/core/transactions/obligations.py +272 -0
- package/src/superlocalmemory/core/transactions/owners.py +114 -0
- package/src/superlocalmemory/core/transactions/reconciler.py +285 -0
- package/src/superlocalmemory/core/transactions/service.py +330 -0
- package/src/superlocalmemory/core/worker_pool.py +33 -5
- package/src/superlocalmemory/encoding/cognitive_consolidator.py +70 -28
- package/src/superlocalmemory/encoding/emotional.py +75 -14
- package/src/superlocalmemory/encoding/scene_builder.py +115 -13
- package/src/superlocalmemory/encoding/temporal_parser.py +4 -0
- package/src/superlocalmemory/evolution/blind_verifier.py +11 -4
- package/src/superlocalmemory/evolution/evolution_store.py +244 -4
- package/src/superlocalmemory/evolution/llm_dispatch.py +40 -0
- package/src/superlocalmemory/evolution/model_selection.py +18 -3
- package/src/superlocalmemory/evolution/mutation_generator.py +3 -0
- package/src/superlocalmemory/evolution/skill_activator.py +270 -0
- package/src/superlocalmemory/evolution/skill_evolver.py +281 -59
- package/src/superlocalmemory/evolution/types.py +30 -8
- package/src/superlocalmemory/graph/cozo_backend.py +17 -9
- package/src/superlocalmemory/hooks/auto_invoker.py +2 -1
- package/src/superlocalmemory/hooks/auto_recall.py +64 -30
- package/src/superlocalmemory/hooks/codex_assets.py +14 -1
- package/src/superlocalmemory/infra/backup.py +434 -7
- package/src/superlocalmemory/infra/process_reaper.py +18 -0
- package/src/superlocalmemory/infra/self_heal.py +401 -0
- package/src/superlocalmemory/learning/feedback.py +52 -9
- package/src/superlocalmemory/loops/engine.py +10 -0
- package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
- package/src/superlocalmemory/mcp/http_transport.py +30 -331
- package/src/superlocalmemory/mcp/profiles.py +5 -0
- package/src/superlocalmemory/mcp/resources.py +8 -0
- package/src/superlocalmemory/mcp/server.py +51 -4
- package/src/superlocalmemory/mcp/shared.py +19 -0
- package/src/superlocalmemory/mcp/tools_active.py +25 -4
- package/src/superlocalmemory/mcp/tools_code_graph.py +26 -18
- package/src/superlocalmemory/mcp/tools_context.py +50 -8
- package/src/superlocalmemory/mcp/tools_core.py +69 -21
- package/src/superlocalmemory/mcp/tools_evolution.py +9 -2
- package/src/superlocalmemory/mcp/tools_learning.py +21 -10
- package/src/superlocalmemory/mcp/tools_loops.py +29 -18
- package/src/superlocalmemory/mcp/tools_mesh.py +8 -0
- package/src/superlocalmemory/mcp/tools_ops.py +115 -0
- package/src/superlocalmemory/mcp/tools_optimize.py +4 -0
- package/src/superlocalmemory/mcp/tools_v28.py +10 -3
- package/src/superlocalmemory/mcp/tools_v3.py +34 -14
- package/src/superlocalmemory/mcp/tools_v33.py +18 -33
- package/src/superlocalmemory/mesh/broker.py +124 -46
- package/src/superlocalmemory/mesh/broker_security.py +470 -0
- package/src/superlocalmemory/mesh/discovery.py +365 -0
- package/src/superlocalmemory/mesh/lock_protocol.py +313 -0
- package/src/superlocalmemory/mesh/node_identity.py +97 -0
- package/src/superlocalmemory/mesh/outbox_remote.py +429 -0
- package/src/superlocalmemory/mesh/remote_sync.py +511 -28
- package/src/superlocalmemory/mesh/state_sync.py +286 -0
- package/src/superlocalmemory/optimize/config/store.py +45 -0
- package/src/superlocalmemory/parameterization/cross_project.py +12 -0
- package/src/superlocalmemory/parameterization/prompt_injector.py +13 -11
- package/src/superlocalmemory/parameterization/prompt_lifecycle.py +8 -2
- package/src/superlocalmemory/parameterization/workflow_miner.py +17 -0
- package/src/superlocalmemory/retrieval/ann_index.py +5 -0
- package/src/superlocalmemory/retrieval/bm25_channel.py +49 -2
- package/src/superlocalmemory/retrieval/engine.py +19 -4
- package/src/superlocalmemory/retrieval/fusion.py +4 -1
- package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -3
- package/src/superlocalmemory/retrieval/remote_reranker.py +47 -22
- package/src/superlocalmemory/retrieval/reranker.py +32 -1
- package/src/superlocalmemory/retrieval/temporal_channel.py +16 -3
- package/src/superlocalmemory/retrieval/temporal_utils.py +107 -0
- package/src/superlocalmemory/retrieval/temporal_validity_filter.py +155 -42
- package/src/superlocalmemory/retrieval/vector_store.py +214 -8
- package/src/superlocalmemory/server/api.py +5 -5
- package/src/superlocalmemory/server/egress_policy.py +258 -0
- package/src/superlocalmemory/server/rbac_enforce.py +32 -0
- package/src/superlocalmemory/server/route_mutations.py +20 -0
- package/src/superlocalmemory/server/routes/compliance.py +153 -7
- package/src/superlocalmemory/server/routes/data_io.py +43 -2
- package/src/superlocalmemory/server/routes/events.py +15 -0
- package/src/superlocalmemory/server/routes/memories.py +56 -3
- package/src/superlocalmemory/server/routes/mesh.py +82 -1
- package/src/superlocalmemory/server/routes/mesh_lock.py +54 -0
- package/src/superlocalmemory/server/routes/mesh_state.py +63 -0
- package/src/superlocalmemory/server/routes/v3_api.py +50 -27
- package/src/superlocalmemory/server/routes/ws.py +86 -0
- package/src/superlocalmemory/server/ui.py +6 -6
- package/src/superlocalmemory/server/unified_daemon.py +942 -119
- package/src/superlocalmemory/storage/_migration_internals.py +568 -0
- package/src/superlocalmemory/storage/_schema_version.py +110 -0
- package/src/superlocalmemory/storage/database.py +329 -24
- package/src/superlocalmemory/storage/embedding_migrator.py +246 -51
- package/src/superlocalmemory/storage/erasure_fence.py +45 -0
- package/src/superlocalmemory/storage/generation_fence.py +63 -0
- package/src/superlocalmemory/storage/migration_runner.py +140 -417
- package/src/superlocalmemory/storage/migrations/M009_model_lineage.py +40 -0
- package/src/superlocalmemory/storage/migrations/M033_projection_transactions.py +148 -0
- package/src/superlocalmemory/storage/migrations/M034_obligation_integrity.py +58 -0
- package/src/superlocalmemory/storage/migrations/M035_erasure_receipts.py +113 -0
- package/src/superlocalmemory/storage/migrations/M036_vector_row_map.py +107 -0
- package/src/superlocalmemory/storage/migrations/M037_manifest_hmac_version.py +162 -0
- package/src/superlocalmemory/storage/migrations/{M033_learning_feedback_channel.py → M038_learning_feedback_channel.py} +3 -3
- package/src/superlocalmemory/storage/migrations/M039_scene_fact_members.py +137 -0
- package/src/superlocalmemory/storage/migrations/__init__.py +4 -2
- package/src/superlocalmemory/storage/schema.py +67 -0
- package/src/superlocalmemory/storage/write_coordinator.py +125 -0
- package/src/superlocalmemory/trust/scorer.py +28 -4
- package/src/superlocalmemory/ui/index.html +14 -3
- package/src/superlocalmemory/ui/js/auto-settings.js +12 -1
- package/src/superlocalmemory/ui/js/brain.js +6 -4
- package/src/superlocalmemory/ui/js/compliance.js +66 -12
- package/src/superlocalmemory/ui/js/dashboard.js +13 -3
- package/src/superlocalmemory/ui/js/feedback.js +8 -2
- package/src/superlocalmemory/ui/js/lifecycle.js +7 -1
- package/src/superlocalmemory/ui/js/modal.js +272 -5
- package/src/superlocalmemory/ui/js/od-backup.js +9 -2
- package/src/superlocalmemory/ui/js/od-compliance-ext.js +301 -0
- package/src/superlocalmemory/ui/js/od-operations.js +154 -23
- package/src/superlocalmemory/ui/js/od-ops-health.js +417 -0
- package/src/superlocalmemory/ui/js/od-optimize.js +35 -21
- package/src/superlocalmemory/ui/js/od-team.js +9 -2
- package/src/superlocalmemory/ui/js/optimize.js +13 -16
- package/src/superlocalmemory/ui/js/profiles.js +7 -3
- package/src/superlocalmemory/ui/js/settings.js +7 -1
- package/src/superlocalmemory/vector/lancedb_backend.py +19 -9
- package/src/superlocalmemory/attribution/mathematical_dna.py +0 -235
- package/src/superlocalmemory/cli/post_install.py +0 -114
- package/src/superlocalmemory/core/clock_monitor.py +0 -45
- package/src/superlocalmemory/core/db_pool.py +0 -80
- package/src/superlocalmemory/core/error_catalog.py +0 -113
- package/src/superlocalmemory/core/loop_watchdog.py +0 -56
- package/src/superlocalmemory/core/priority_queue.py +0 -61
- package/src/superlocalmemory/core/pruning_engine.py +0 -216
- package/src/superlocalmemory/core/queue_dispatcher.py +0 -73
- package/src/superlocalmemory/core/slmignore.py +0 -125
- package/src/superlocalmemory/infra/heartbeat_monitor.py +0 -140
- package/src/superlocalmemory/infra/webhook_dispatcher.py +0 -247
- package/src/superlocalmemory/learning/quantization_scheduler.py +0 -320
- package/src/superlocalmemory/storage/access_control.py +0 -182
|
@@ -32,7 +32,7 @@ if "OMP_NUM_THREADS" not in os.environ:
|
|
|
32
32
|
os.environ["OMP_NUM_THREADS"] = "2"
|
|
33
33
|
# ---------------------------------------------------------------------------
|
|
34
34
|
|
|
35
|
-
__version__ = "
|
|
35
|
+
__version__ = "4.0.0"
|
|
36
36
|
|
|
37
37
|
_REQUIRED_VERSIONS = {
|
|
38
38
|
"sentence_transformers": "5.3.0",
|
|
@@ -270,6 +270,12 @@ def _cmd_loop(args: Namespace) -> None:
|
|
|
270
270
|
cmd_loop(args)
|
|
271
271
|
|
|
272
272
|
|
|
273
|
+
def _cmd_ops(args: Namespace) -> None:
|
|
274
|
+
"""Wave-3: operational recovery & admin remediation commands."""
|
|
275
|
+
from superlocalmemory.cli.ops_cmd import cmd_ops
|
|
276
|
+
cmd_ops(args)
|
|
277
|
+
|
|
278
|
+
|
|
273
279
|
# ---- end SLM v3.6 Optimize dispatch functions ----
|
|
274
280
|
|
|
275
281
|
|
|
@@ -399,6 +405,8 @@ def dispatch(args: Namespace) -> None:
|
|
|
399
405
|
"loop": _cmd_loop,
|
|
400
406
|
# V3.8.2 super-help — grouped overview of every command + topics
|
|
401
407
|
"help": cmd_help,
|
|
408
|
+
# Wave-3: operational recovery & admin remediation
|
|
409
|
+
"ops": _cmd_ops,
|
|
402
410
|
}
|
|
403
411
|
handler = handlers.get(args.command)
|
|
404
412
|
if handler:
|
|
@@ -982,6 +990,10 @@ def cmd_evolve(args: Namespace) -> None:
|
|
|
982
990
|
if not session_id:
|
|
983
991
|
return # Silent exit — nothing to do without a session
|
|
984
992
|
|
|
993
|
+
from superlocalmemory.core.admission import gate_cli_mutation
|
|
994
|
+
from superlocalmemory.core.operation_request import OperationKind
|
|
995
|
+
gate_cli_mutation(OperationKind.EVOLVE_SKILL)
|
|
996
|
+
|
|
985
997
|
# Check if evolution is enabled via config.json
|
|
986
998
|
config_path = state_path("config.json")
|
|
987
999
|
try:
|
|
@@ -1059,6 +1071,11 @@ def cmd_mode(args: Namespace) -> None:
|
|
|
1059
1071
|
|
|
1060
1072
|
config = SLMConfig.load()
|
|
1061
1073
|
|
|
1074
|
+
if args.value:
|
|
1075
|
+
from superlocalmemory.core.admission import gate_cli_mutation
|
|
1076
|
+
from superlocalmemory.core.operation_request import OperationKind
|
|
1077
|
+
gate_cli_mutation(OperationKind.MODE_CHANGE)
|
|
1078
|
+
|
|
1062
1079
|
if getattr(args, 'json', False):
|
|
1063
1080
|
from superlocalmemory.cli.json_output import json_print
|
|
1064
1081
|
if args.value:
|
|
@@ -1431,6 +1448,9 @@ def cmd_recall(args: Namespace) -> None:
|
|
|
1431
1448
|
# produces True/False here.
|
|
1432
1449
|
include_global = getattr(args, 'include_global', None)
|
|
1433
1450
|
include_shared = getattr(args, 'include_shared', None)
|
|
1451
|
+
# Phase-1/D2: clamp cross-profile scope flags before forwarding to daemon.
|
|
1452
|
+
from superlocalmemory.core.admission import enforce_read_scope
|
|
1453
|
+
include_global, include_shared = enforce_read_scope(include_global, include_shared)
|
|
1434
1454
|
|
|
1435
1455
|
# V3.3.21: Route through daemon for instant response (no cold start).
|
|
1436
1456
|
# S9-DASH-02: pass a stable session_id derived from the shell's
|
|
@@ -1453,10 +1473,23 @@ def cmd_recall(args: Namespace) -> None:
|
|
|
1453
1473
|
scope_qs += f"&include_shared={str(include_shared).lower()}"
|
|
1454
1474
|
_window = getattr(args, "window", "") or ""
|
|
1455
1475
|
window_qs = f"&window={quote(_window)}" if _window else ""
|
|
1476
|
+
_as_of = getattr(args, "as_of", "") or ""
|
|
1477
|
+
if _as_of:
|
|
1478
|
+
from superlocalmemory.retrieval.temporal_utils import normalize_as_of
|
|
1479
|
+
_as_of_norm = normalize_as_of(_as_of)
|
|
1480
|
+
if _as_of_norm is None:
|
|
1481
|
+
import sys as _sys
|
|
1482
|
+
_sys.stderr.write(
|
|
1483
|
+
f"Error: invalid --as-of value: {_as_of!r}\n"
|
|
1484
|
+
"Expected ISO 8601 UTC datetime, e.g. '2024-01-01T00:00:00Z'.\n"
|
|
1485
|
+
)
|
|
1486
|
+
_sys.exit(1)
|
|
1487
|
+
_as_of = _as_of_norm
|
|
1488
|
+
as_of_qs = f"&as_of={quote(_as_of)}" if _as_of else ""
|
|
1456
1489
|
result = daemon_request(
|
|
1457
1490
|
"GET",
|
|
1458
1491
|
f"/recall?q={quote(args.query)}&limit={args.limit}"
|
|
1459
|
-
f"&session_id={quote(session_id)}{fast_qs}{scope_qs}{window_qs}",
|
|
1492
|
+
f"&session_id={quote(session_id)}{fast_qs}{scope_qs}{window_qs}{as_of_qs}",
|
|
1460
1493
|
)
|
|
1461
1494
|
if result and "results" in result:
|
|
1462
1495
|
# Format daemon response same as engine response
|
|
@@ -1522,6 +1555,10 @@ def _cli_record_signals(config, query, results):
|
|
|
1522
1555
|
|
|
1523
1556
|
def cmd_forget(args: Namespace) -> None:
|
|
1524
1557
|
"""Delete daemon-queried memories matching a query."""
|
|
1558
|
+
from superlocalmemory.core.admission import gate_cli_mutation
|
|
1559
|
+
from superlocalmemory.core.operation_request import OperationKind
|
|
1560
|
+
gate_cli_mutation(OperationKind.FORGET)
|
|
1561
|
+
|
|
1525
1562
|
import urllib.parse
|
|
1526
1563
|
|
|
1527
1564
|
from superlocalmemory.cli.daemon import (
|
|
@@ -1644,6 +1681,10 @@ def cmd_forget(args: Namespace) -> None:
|
|
|
1644
1681
|
|
|
1645
1682
|
def cmd_delete(args: Namespace) -> None:
|
|
1646
1683
|
"""Delete a specific memory by exact fact ID."""
|
|
1684
|
+
from superlocalmemory.core.admission import gate_cli_mutation
|
|
1685
|
+
from superlocalmemory.core.operation_request import OperationKind
|
|
1686
|
+
gate_cli_mutation(OperationKind.FORGET)
|
|
1687
|
+
|
|
1647
1688
|
import urllib.parse
|
|
1648
1689
|
|
|
1649
1690
|
from superlocalmemory.cli.daemon import (
|
|
@@ -1711,6 +1752,10 @@ def cmd_delete(args: Namespace) -> None:
|
|
|
1711
1752
|
|
|
1712
1753
|
def cmd_update(args: Namespace) -> None:
|
|
1713
1754
|
"""Update the content of a specific memory by exact fact ID."""
|
|
1755
|
+
from superlocalmemory.core.admission import gate_cli_mutation
|
|
1756
|
+
from superlocalmemory.core.operation_request import OperationKind
|
|
1757
|
+
gate_cli_mutation(OperationKind.CORRECT)
|
|
1758
|
+
|
|
1714
1759
|
import urllib.parse
|
|
1715
1760
|
|
|
1716
1761
|
from superlocalmemory.cli.daemon import (
|
|
@@ -1863,7 +1908,7 @@ def cmd_status(args: Namespace) -> None:
|
|
|
1863
1908
|
])
|
|
1864
1909
|
return
|
|
1865
1910
|
|
|
1866
|
-
print("SuperLocalMemory
|
|
1911
|
+
print("SuperLocalMemory V4")
|
|
1867
1912
|
print(f" Mode: {config.mode.value.upper()}")
|
|
1868
1913
|
print(f" Provider: {config.llm.provider or 'none'}")
|
|
1869
1914
|
print(
|
|
@@ -2023,14 +2068,36 @@ def _gather_optimize_surface_b() -> dict:
|
|
|
2023
2068
|
def _readline_with_timeout(
|
|
2024
2069
|
stream, timeout_sec: float,
|
|
2025
2070
|
) -> tuple[str | None, Exception | None]:
|
|
2026
|
-
"""Read one line from a pipe-like stream
|
|
2071
|
+
"""Read one line from a pipe-like stream with a deadline.
|
|
2027
2072
|
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2073
|
+
Prefer a selector poll of the stream's file descriptor on POSIX so a
|
|
2074
|
+
timeout never abandons a thread blocked in readline() (thread + FD leak).
|
|
2075
|
+
Windows select() only accepts sockets, not subprocess pipes — fall back
|
|
2076
|
+
to a bounded helper thread there and for streams without a usable fileno.
|
|
2031
2077
|
"""
|
|
2078
|
+
import selectors
|
|
2032
2079
|
import threading
|
|
2033
2080
|
|
|
2081
|
+
timeout_sec = max(0.0, float(timeout_sec))
|
|
2082
|
+
fd: int | None
|
|
2083
|
+
try:
|
|
2084
|
+
raw_fd = stream.fileno()
|
|
2085
|
+
fd = raw_fd if isinstance(raw_fd, int) else None
|
|
2086
|
+
except (AttributeError, OSError, ValueError, TypeError):
|
|
2087
|
+
fd = None
|
|
2088
|
+
|
|
2089
|
+
if fd is not None and sys.platform != "win32":
|
|
2090
|
+
try:
|
|
2091
|
+
with selectors.DefaultSelector() as sel:
|
|
2092
|
+
sel.register(fd, selectors.EVENT_READ)
|
|
2093
|
+
events = sel.select(timeout=timeout_sec)
|
|
2094
|
+
if not events:
|
|
2095
|
+
return None, None
|
|
2096
|
+
line = stream.readline()
|
|
2097
|
+
return (line if isinstance(line, str) else None), None
|
|
2098
|
+
except (OSError, ValueError) as exc:
|
|
2099
|
+
return None, exc
|
|
2100
|
+
|
|
2034
2101
|
result: dict[str, object] = {}
|
|
2035
2102
|
|
|
2036
2103
|
def _read() -> None:
|
|
@@ -2091,6 +2158,7 @@ _COMMAND_GROUPS: list[tuple[str, list[tuple[str, str]]]] = [
|
|
|
2091
2158
|
("Health & self-healing", [
|
|
2092
2159
|
("doctor", "Full pre-flight check (add --fix to auto-repair)"),
|
|
2093
2160
|
("health", "Quick math/retrieval layer status"),
|
|
2161
|
+
("ops", "List/resolve stuck, dead-lettered, or degraded operations (admin)"),
|
|
2094
2162
|
("diagnostics", "Export a local diagnostics bundle"),
|
|
2095
2163
|
("evidence", "Build/inspect evidence bundles"),
|
|
2096
2164
|
("rotate-token", "Rotate the local dashboard install token"),
|
|
@@ -2207,7 +2275,7 @@ def cmd_help(args: Namespace) -> None:
|
|
|
2207
2275
|
return
|
|
2208
2276
|
|
|
2209
2277
|
from superlocalmemory.cli.json_output import _get_version
|
|
2210
|
-
print(f"SuperLocalMemory
|
|
2278
|
+
print(f"SuperLocalMemory V4 ({_get_version()}) — command overview")
|
|
2211
2279
|
print("=" * 58)
|
|
2212
2280
|
print("Run any command with -h for its options, e.g. slm recall -h\n")
|
|
2213
2281
|
for title, rows in _COMMAND_GROUPS:
|
|
@@ -2254,7 +2322,7 @@ def cmd_doctor(args: Namespace) -> None:
|
|
|
2254
2322
|
print(line)
|
|
2255
2323
|
|
|
2256
2324
|
if not use_json:
|
|
2257
|
-
print("SuperLocalMemory
|
|
2325
|
+
print("SuperLocalMemory V4 — Doctor (Pre-flight Check)")
|
|
2258
2326
|
print("=" * 50)
|
|
2259
2327
|
print()
|
|
2260
2328
|
|
|
@@ -2286,6 +2354,32 @@ def cmd_doctor(args: Namespace) -> None:
|
|
|
2286
2354
|
if not use_json:
|
|
2287
2355
|
print(f" auto-repair error: {_fix_exc}\n")
|
|
2288
2356
|
|
|
2357
|
+
# H5 — stale-artifact reap (additive; runs after component healer).
|
|
2358
|
+
# Removes provably-dead lock/PID files so the doctor report below
|
|
2359
|
+
# reflects the healed state. Fail-open — never blocks doctor.
|
|
2360
|
+
try:
|
|
2361
|
+
from superlocalmemory.infra.self_heal import reap_stale_artifacts
|
|
2362
|
+
from superlocalmemory.infra.data_root import canonical_data_root
|
|
2363
|
+
|
|
2364
|
+
_sh_report = reap_stale_artifacts(canonical_data_root())
|
|
2365
|
+
if not use_json:
|
|
2366
|
+
if _sh_report["removed"]:
|
|
2367
|
+
print(
|
|
2368
|
+
f" Cleaned {len(_sh_report['removed'])} stale"
|
|
2369
|
+
" lock/pid file(s):"
|
|
2370
|
+
)
|
|
2371
|
+
for _item in _sh_report["removed"]:
|
|
2372
|
+
print(
|
|
2373
|
+
f" - {Path(_item['path']).name}"
|
|
2374
|
+
f" ({_item['reason']})"
|
|
2375
|
+
)
|
|
2376
|
+
else:
|
|
2377
|
+
print(" No stale lock/pid files found.")
|
|
2378
|
+
print()
|
|
2379
|
+
except Exception as _sh_exc:
|
|
2380
|
+
if not use_json:
|
|
2381
|
+
print(f" stale-artifact cleanup skipped: {_sh_exc}\n")
|
|
2382
|
+
|
|
2289
2383
|
# 1. Python version
|
|
2290
2384
|
v = sys.version_info
|
|
2291
2385
|
if v >= (3, 11):
|
|
@@ -2884,7 +2978,7 @@ def cmd_warmup(_args: Namespace) -> None:
|
|
|
2884
2978
|
"""
|
|
2885
2979
|
import superlocalmemory.core.embeddings as _emb_mod
|
|
2886
2980
|
|
|
2887
|
-
print("SuperLocalMemory
|
|
2981
|
+
print("SuperLocalMemory V4 — Embedding Model Warmup")
|
|
2888
2982
|
print("=" * 50)
|
|
2889
2983
|
print(f" Python: {sys.executable}")
|
|
2890
2984
|
print(f" Model: nomic-ai/nomic-embed-text-v1.5 (~500MB)")
|
|
@@ -2994,10 +3088,10 @@ def cmd_dashboard(args: Namespace) -> None:
|
|
|
2994
3088
|
|
|
2995
3089
|
port = getattr(args, "port", None) or _get_port()
|
|
2996
3090
|
|
|
2997
|
-
print(" SuperLocalMemory
|
|
3091
|
+
print(" SuperLocalMemory V4 — Web Dashboard")
|
|
2998
3092
|
print(f" Starting daemon if needed...")
|
|
2999
3093
|
|
|
3000
|
-
if not ensure_daemon():
|
|
3094
|
+
if not ensure_daemon(port=port):
|
|
3001
3095
|
print(" ✗ Could not start daemon. Run `slm doctor` to diagnose.")
|
|
3002
3096
|
sys.exit(1)
|
|
3003
3097
|
|
|
@@ -3057,6 +3151,12 @@ def cmd_profile(args: Namespace) -> None:
|
|
|
3057
3151
|
Writes to BOTH SQLite and profiles.json so CLI, Dashboard, and
|
|
3058
3152
|
MCP all see the same profiles.
|
|
3059
3153
|
"""
|
|
3154
|
+
action = getattr(args, "action", "list")
|
|
3155
|
+
if action in ("switch", "create"):
|
|
3156
|
+
from superlocalmemory.core.admission import gate_cli_mutation
|
|
3157
|
+
from superlocalmemory.core.operation_request import OperationKind
|
|
3158
|
+
gate_cli_mutation(OperationKind.PROFILE_SWITCH)
|
|
3159
|
+
|
|
3060
3160
|
from superlocalmemory.core.config import SLMConfig
|
|
3061
3161
|
from superlocalmemory.storage.database import DatabaseManager
|
|
3062
3162
|
from superlocalmemory.storage import schema
|
|
@@ -3598,6 +3698,10 @@ def cmd_observe(args: Namespace) -> None:
|
|
|
3598
3698
|
print("No content to observe.")
|
|
3599
3699
|
return
|
|
3600
3700
|
|
|
3701
|
+
from superlocalmemory.core.admission import gate_cli_mutation
|
|
3702
|
+
from superlocalmemory.core.operation_request import OperationKind
|
|
3703
|
+
gate_cli_mutation(OperationKind.REMEMBER)
|
|
3704
|
+
|
|
3601
3705
|
# V3.3.28: Route through daemon (singleton engine, single embedding worker).
|
|
3602
3706
|
# This is the P0 fix for the memory blast incident of April 7, 2026.
|
|
3603
3707
|
try:
|
|
@@ -3665,6 +3769,9 @@ def cmd_observe(args: Namespace) -> None:
|
|
|
3665
3769
|
|
|
3666
3770
|
def cmd_decay(args: Namespace) -> None:
|
|
3667
3771
|
"""Run Ebbinghaus forgetting decay cycle."""
|
|
3772
|
+
from superlocalmemory.core.admission import gate_cli_mutation
|
|
3773
|
+
from superlocalmemory.core.operation_request import OperationKind
|
|
3774
|
+
gate_cli_mutation(OperationKind.CONSOLIDATE)
|
|
3668
3775
|
from superlocalmemory.core.config import SLMConfig
|
|
3669
3776
|
from superlocalmemory.core.engine import MemoryEngine
|
|
3670
3777
|
|
|
@@ -3721,6 +3828,9 @@ def cmd_decay(args: Namespace) -> None:
|
|
|
3721
3828
|
|
|
3722
3829
|
def cmd_quantize(args: Namespace) -> None:
|
|
3723
3830
|
"""Run EAP embedding quantization cycle."""
|
|
3831
|
+
from superlocalmemory.core.admission import gate_cli_mutation
|
|
3832
|
+
from superlocalmemory.core.operation_request import OperationKind
|
|
3833
|
+
gate_cli_mutation(OperationKind.CONSOLIDATE)
|
|
3724
3834
|
from superlocalmemory.core.config import SLMConfig
|
|
3725
3835
|
from superlocalmemory.core.engine import MemoryEngine
|
|
3726
3836
|
|
|
@@ -3779,6 +3889,10 @@ def cmd_quantize(args: Namespace) -> None:
|
|
|
3779
3889
|
|
|
3780
3890
|
def cmd_consolidate(args: Namespace) -> None:
|
|
3781
3891
|
"""Run cognitive consolidation pipeline."""
|
|
3892
|
+
from superlocalmemory.core.admission import gate_cli_mutation
|
|
3893
|
+
from superlocalmemory.core.operation_request import OperationKind
|
|
3894
|
+
gate_cli_mutation(OperationKind.CONSOLIDATE)
|
|
3895
|
+
|
|
3782
3896
|
from superlocalmemory.core.config import SLMConfig
|
|
3783
3897
|
from superlocalmemory.core.engine import MemoryEngine
|
|
3784
3898
|
|
|
@@ -508,9 +508,13 @@ def _start_daemon_subprocess() -> bool:
|
|
|
508
508
|
return _wait_for_daemon(timeout=60)
|
|
509
509
|
|
|
510
510
|
|
|
511
|
-
def ensure_daemon() -> bool:
|
|
511
|
+
def ensure_daemon(*, port: int | None = None) -> bool:
|
|
512
512
|
"""Start daemon if not running. Returns True if daemon is ready.
|
|
513
513
|
|
|
514
|
+
``port`` — when supplied, the daemon is started (or verified) on this port
|
|
515
|
+
instead of the configured default. The dashboard passes its own ``--port``
|
|
516
|
+
here so the bind authority matches the URL shown to the user.
|
|
517
|
+
|
|
514
518
|
v3.4.4 BULLETPROOF:
|
|
515
519
|
1. If PID alive → return True immediately (even if warming up)
|
|
516
520
|
2. File lock prevents two callers from starting concurrent daemons
|
|
@@ -179,7 +179,7 @@ def main() -> None:
|
|
|
179
179
|
|
|
180
180
|
parser = argparse.ArgumentParser(
|
|
181
181
|
prog="slm",
|
|
182
|
-
description=f"SuperLocalMemory
|
|
182
|
+
description=f"SuperLocalMemory V4 ({_ver}) — AI agent memory with mathematical foundations",
|
|
183
183
|
epilog=_HELP_EPILOG,
|
|
184
184
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
185
185
|
)
|
|
@@ -388,6 +388,12 @@ def main() -> None:
|
|
|
388
388
|
"(24h, 7d, 30d, 1y) or an explicit range (2026-07-01..2026-07-31). "
|
|
389
389
|
"Default: no time filter.",
|
|
390
390
|
)
|
|
391
|
+
recall_p.add_argument(
|
|
392
|
+
"--as-of", dest="as_of", default="",
|
|
393
|
+
help="Point-in-time recall: an ISO-8601 timestamp "
|
|
394
|
+
"(2026-01-01T00:00:00+00:00) that pins retrieval to a temporal "
|
|
395
|
+
"snapshot. Default: current-state recall.",
|
|
396
|
+
)
|
|
391
397
|
recall_p.add_argument(
|
|
392
398
|
"--fast", action="store_true",
|
|
393
399
|
help="Force-skip the internal agentic verification round (all six retrieval "
|
|
@@ -513,7 +519,7 @@ def main() -> None:
|
|
|
513
519
|
sub.add_parser("mcp", help="Start MCP server (stdio transport for IDE integration)")
|
|
514
520
|
sub.add_parser("warmup", help="Pre-download embedding model (~500MB, one-time)")
|
|
515
521
|
|
|
516
|
-
dashboard_p = sub.add_parser("dashboard", help="Open
|
|
522
|
+
dashboard_p = sub.add_parser("dashboard", help="Open web dashboard")
|
|
517
523
|
dashboard_p.add_argument(
|
|
518
524
|
"--port", type=int, default=8765, help="Port (default 8765)",
|
|
519
525
|
)
|
|
@@ -903,6 +909,33 @@ def main() -> None:
|
|
|
903
909
|
_sp.add_argument("--json", action="store_true",
|
|
904
910
|
help="Output structured JSON (agent-native)")
|
|
905
911
|
|
|
912
|
+
# Wave-3: operational recovery & admin remediation
|
|
913
|
+
ops_p = sub.add_parser(
|
|
914
|
+
"ops",
|
|
915
|
+
help="Operational recovery: list failures, resolve stuck ops, check status",
|
|
916
|
+
)
|
|
917
|
+
ops_sub = ops_p.add_subparsers(dest="ops_command", title="ops subcommands")
|
|
918
|
+
ops_list_p = ops_sub.add_parser(
|
|
919
|
+
"list", help="List all failed, stuck, or degraded operations (admin)")
|
|
920
|
+
ops_list_p.add_argument(
|
|
921
|
+
"--profile", default=None, metavar="PROFILE",
|
|
922
|
+
help="Filter results to a specific profile (default: all)")
|
|
923
|
+
ops_list_p.add_argument(
|
|
924
|
+
"--json", action="store_true", help="Output structured JSON (agent-native)")
|
|
925
|
+
ops_resolve_p = ops_sub.add_parser(
|
|
926
|
+
"resolve", help="Admin action: retry / force-reconcile / cancel a stuck op")
|
|
927
|
+
ops_resolve_p.add_argument("operation_id", help="Operation ID from slm ops list")
|
|
928
|
+
ops_resolve_p.add_argument(
|
|
929
|
+
"--action", required=True,
|
|
930
|
+
choices=["retry", "force_reconcile", "cancel"],
|
|
931
|
+
help="Remediation action to apply")
|
|
932
|
+
ops_resolve_p.add_argument(
|
|
933
|
+
"--json", action="store_true", help="Output structured JSON (agent-native)")
|
|
934
|
+
ops_status_p = ops_sub.add_parser(
|
|
935
|
+
"status", help="Quick failure count + writer stall overview")
|
|
936
|
+
ops_status_p.add_argument(
|
|
937
|
+
"--json", action="store_true", help="Output structured JSON (agent-native)")
|
|
938
|
+
|
|
906
939
|
args = parser.parse_args()
|
|
907
940
|
|
|
908
941
|
if not args.command:
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
+
# Part of SuperLocalMemory V4 | https://qualixar.com | https://varunpratap.com
|
|
4
|
+
|
|
5
|
+
"""``slm ops`` — operational recovery & admin remediation.
|
|
6
|
+
|
|
7
|
+
Subcommands:
|
|
8
|
+
|
|
9
|
+
* ``slm ops list [--profile P]``
|
|
10
|
+
Show all failed, stuck, or degraded operations grouped by category.
|
|
11
|
+
Proxies GET /operations/failed on the running daemon.
|
|
12
|
+
|
|
13
|
+
* ``slm ops resolve <id> --action {retry|force_reconcile|cancel}``
|
|
14
|
+
Admin action on a specific operation.
|
|
15
|
+
Proxies POST /operations/<id>/resolve on the running daemon.
|
|
16
|
+
|
|
17
|
+
* ``slm ops status``
|
|
18
|
+
Quick overview: failure counts + writer stall state from /status.
|
|
19
|
+
No authentication required (status is public).
|
|
20
|
+
|
|
21
|
+
RBAC: list / resolve require OWNER or ADMIN role on the daemon.
|
|
22
|
+
Unauthenticated users see a clear permission error.
|
|
23
|
+
|
|
24
|
+
Part of Qualixar | Author: Varun Pratap Bhardwaj
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import json as _json
|
|
30
|
+
import sys
|
|
31
|
+
import urllib.error as _uerr
|
|
32
|
+
import urllib.request as _urq
|
|
33
|
+
from argparse import Namespace
|
|
34
|
+
from typing import Any
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
_VALID_ACTIONS = ("retry", "force_reconcile", "cancel")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
# Internal helpers
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
|
|
44
|
+
def _get_daemon_port() -> int:
|
|
45
|
+
"""Return the active daemon port (default 8765)."""
|
|
46
|
+
try:
|
|
47
|
+
from superlocalmemory.cli.daemon import _get_port
|
|
48
|
+
return _get_port()
|
|
49
|
+
except Exception:
|
|
50
|
+
return 8765
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _daemon_get(path: str, timeout_s: float = 10.0) -> dict | None:
|
|
54
|
+
"""HTTP GET to the daemon; return parsed JSON or None on failure."""
|
|
55
|
+
port = _get_daemon_port()
|
|
56
|
+
url = f"http://127.0.0.1:{port}{path}"
|
|
57
|
+
try:
|
|
58
|
+
with _urq.urlopen(url, timeout=timeout_s) as resp: # noqa: S310
|
|
59
|
+
raw = resp.read().decode()
|
|
60
|
+
return _json.loads(raw)
|
|
61
|
+
except _uerr.HTTPError as exc:
|
|
62
|
+
if exc.code == 403:
|
|
63
|
+
_die(
|
|
64
|
+
"Permission denied: list/resolve requires OWNER or ADMIN role.\n"
|
|
65
|
+
"Check your SLM credentials or ask your administrator."
|
|
66
|
+
)
|
|
67
|
+
body = exc.read().decode(errors="replace") if hasattr(exc, "read") else str(exc)
|
|
68
|
+
_die(f"Daemon returned HTTP {exc.code}: {body}")
|
|
69
|
+
except _uerr.URLError as exc:
|
|
70
|
+
_die(
|
|
71
|
+
f"Could not reach SLM daemon at {url}: {exc.reason}\n"
|
|
72
|
+
"Make sure the daemon is running: slm serve"
|
|
73
|
+
)
|
|
74
|
+
return None # unreachable; _die exits
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _daemon_post(path: str, body: dict, timeout_s: float = 10.0) -> dict | None:
|
|
78
|
+
"""HTTP POST to the daemon; return parsed JSON or None on failure."""
|
|
79
|
+
port = _get_daemon_port()
|
|
80
|
+
url = f"http://127.0.0.1:{port}{path}"
|
|
81
|
+
try:
|
|
82
|
+
req = _urq.Request(
|
|
83
|
+
url,
|
|
84
|
+
data=_json.dumps(body).encode(),
|
|
85
|
+
headers={"Content-Type": "application/json"},
|
|
86
|
+
method="POST",
|
|
87
|
+
)
|
|
88
|
+
with _urq.urlopen(req, timeout=timeout_s) as resp: # noqa: S310
|
|
89
|
+
raw = resp.read().decode()
|
|
90
|
+
return _json.loads(raw)
|
|
91
|
+
except _uerr.HTTPError as exc:
|
|
92
|
+
if exc.code == 403:
|
|
93
|
+
_die(
|
|
94
|
+
"Permission denied: resolve requires OWNER or ADMIN role.\n"
|
|
95
|
+
"Check your SLM credentials or ask your administrator."
|
|
96
|
+
)
|
|
97
|
+
if exc.code == 400:
|
|
98
|
+
body_txt = exc.read().decode(errors="replace") if hasattr(exc, "read") else str(exc)
|
|
99
|
+
_die(f"Bad request: {body_txt}")
|
|
100
|
+
body_txt = exc.read().decode(errors="replace") if hasattr(exc, "read") else str(exc)
|
|
101
|
+
_die(f"Daemon returned HTTP {exc.code}: {body_txt}")
|
|
102
|
+
except _uerr.URLError as exc:
|
|
103
|
+
_die(
|
|
104
|
+
f"Could not reach SLM daemon at {url}: {exc.reason}\n"
|
|
105
|
+
"Make sure the daemon is running: slm serve"
|
|
106
|
+
)
|
|
107
|
+
return None # unreachable; _die exits
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _die(message: str) -> None:
|
|
111
|
+
print(f"error: {message}", file=sys.stderr)
|
|
112
|
+
sys.exit(1)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _print_json(data: Any) -> None:
|
|
116
|
+
print(_json.dumps(data, indent=2, default=str))
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# ---------------------------------------------------------------------------
|
|
120
|
+
# Subcommand handlers
|
|
121
|
+
# ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
def _cmd_ops_list(args: Namespace) -> None:
|
|
124
|
+
"""List all failed, stuck, or degraded operations."""
|
|
125
|
+
profile = getattr(args, "profile", None)
|
|
126
|
+
path = "/operations/failed"
|
|
127
|
+
if profile:
|
|
128
|
+
path = f"{path}?profile={profile}"
|
|
129
|
+
|
|
130
|
+
data = _daemon_get(path)
|
|
131
|
+
if data is None:
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
if getattr(args, "json", False):
|
|
135
|
+
_print_json(data)
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
total: int = data.get("total", 0)
|
|
139
|
+
if total == 0:
|
|
140
|
+
print("All operations healthy. No failures detected.")
|
|
141
|
+
return
|
|
142
|
+
|
|
143
|
+
print(f"Failed operations: {total} total\n")
|
|
144
|
+
|
|
145
|
+
dead_letter = data.get("dead_letter", [])
|
|
146
|
+
if dead_letter:
|
|
147
|
+
print(f"--- Dead-letter (ingestion exhausted, {len(dead_letter)}) ---")
|
|
148
|
+
for entry in dead_letter:
|
|
149
|
+
print(
|
|
150
|
+
f" [{entry.get('operation_id', '?')}] "
|
|
151
|
+
f"type={entry.get('operation_type', '?')} "
|
|
152
|
+
f"attempts={entry.get('attempts', '?')} "
|
|
153
|
+
f"profile={entry.get('profile_id', '?')}"
|
|
154
|
+
)
|
|
155
|
+
if entry.get("error"):
|
|
156
|
+
print(f" error: {entry['error']}")
|
|
157
|
+
print()
|
|
158
|
+
|
|
159
|
+
degraded = data.get("degraded_manifests", [])
|
|
160
|
+
if degraded:
|
|
161
|
+
print(f"--- Degraded manifests ({len(degraded)}) ---")
|
|
162
|
+
for entry in degraded:
|
|
163
|
+
print(
|
|
164
|
+
f" [{entry.get('operation_id', '?')}] "
|
|
165
|
+
f"state={entry.get('state', '?')} "
|
|
166
|
+
f"profile={entry.get('profile_id', '?')}"
|
|
167
|
+
)
|
|
168
|
+
print()
|
|
169
|
+
|
|
170
|
+
exhausted = data.get("exhausted_obligations", [])
|
|
171
|
+
if exhausted:
|
|
172
|
+
print(f"--- Exhausted projection obligations ({len(exhausted)}) ---")
|
|
173
|
+
for entry in exhausted:
|
|
174
|
+
print(
|
|
175
|
+
f" [{entry.get('operation_id', '?')}] "
|
|
176
|
+
f"kind={entry.get('kind', '?')} "
|
|
177
|
+
f"attempts={entry.get('attempts', '?')} "
|
|
178
|
+
f"profile={entry.get('profile_id', '?')}"
|
|
179
|
+
)
|
|
180
|
+
print()
|
|
181
|
+
|
|
182
|
+
print("Use `slm ops resolve <id> --action cancel|retry|force_reconcile` to remediate.")
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _cmd_ops_resolve(args: Namespace) -> None:
|
|
186
|
+
"""Admin action on a specific operation."""
|
|
187
|
+
operation_id: str = args.operation_id
|
|
188
|
+
action: str = args.action
|
|
189
|
+
|
|
190
|
+
if action not in _VALID_ACTIONS:
|
|
191
|
+
_die(f"--action must be one of: {', '.join(_VALID_ACTIONS)}")
|
|
192
|
+
|
|
193
|
+
result = _daemon_post(
|
|
194
|
+
f"/operations/{operation_id}/resolve",
|
|
195
|
+
{"action": action},
|
|
196
|
+
)
|
|
197
|
+
if result is None:
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
if getattr(args, "json", False):
|
|
201
|
+
_print_json(result)
|
|
202
|
+
return
|
|
203
|
+
|
|
204
|
+
success = result.get("success", False)
|
|
205
|
+
if success:
|
|
206
|
+
print(
|
|
207
|
+
f"OK: operation {operation_id!r} resolved with action={action!r}. "
|
|
208
|
+
f"{result.get('message', '')}"
|
|
209
|
+
)
|
|
210
|
+
else:
|
|
211
|
+
reason = result.get("reason") or result.get("error") or "unknown reason"
|
|
212
|
+
print(f"Resolve failed: {reason}", file=sys.stderr)
|
|
213
|
+
sys.exit(1)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _cmd_ops_status(args: Namespace) -> None:
|
|
217
|
+
"""Quick ops-focused health status from the daemon."""
|
|
218
|
+
data = _daemon_get("/status")
|
|
219
|
+
if data is None:
|
|
220
|
+
return
|
|
221
|
+
|
|
222
|
+
fields = {
|
|
223
|
+
"dead_letter_count": data.get("dead_letter_count", 0),
|
|
224
|
+
"degraded_operations": data.get("degraded_operations", 0),
|
|
225
|
+
"exhausted_obligations": data.get("exhausted_obligations", 0),
|
|
226
|
+
"writer_stalled": data.get("writer_stalled", False),
|
|
227
|
+
"writer_stalled_op_id": data.get("writer_stalled_op_id"),
|
|
228
|
+
"writer_stalled_age_s": data.get("writer_stalled_age_s"),
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if getattr(args, "json", False):
|
|
232
|
+
_print_json(fields)
|
|
233
|
+
return
|
|
234
|
+
|
|
235
|
+
total_issues = (
|
|
236
|
+
fields["dead_letter_count"]
|
|
237
|
+
+ fields["degraded_operations"]
|
|
238
|
+
+ fields["exhausted_obligations"]
|
|
239
|
+
)
|
|
240
|
+
stalled = fields["writer_stalled"]
|
|
241
|
+
|
|
242
|
+
if total_issues == 0 and not stalled:
|
|
243
|
+
print("Operations status: HEALTHY — no failures detected.")
|
|
244
|
+
return
|
|
245
|
+
|
|
246
|
+
print("Operations status: DEGRADED\n")
|
|
247
|
+
if fields["dead_letter_count"]:
|
|
248
|
+
print(f" dead-letter entries : {fields['dead_letter_count']}")
|
|
249
|
+
if fields["degraded_operations"]:
|
|
250
|
+
print(f" degraded manifests : {fields['degraded_operations']}")
|
|
251
|
+
if fields["exhausted_obligations"]:
|
|
252
|
+
print(f" exhausted obligations : {fields['exhausted_obligations']}")
|
|
253
|
+
if stalled:
|
|
254
|
+
op_id = fields["writer_stalled_op_id"] or "?"
|
|
255
|
+
age = fields["writer_stalled_age_s"]
|
|
256
|
+
age_str = f" (age {age:.1f}s)" if age is not None else ""
|
|
257
|
+
print(f" writer STALLED : op={op_id}{age_str}")
|
|
258
|
+
print("\nRun `slm ops list` to see details, or check the dashboard.")
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
# ---------------------------------------------------------------------------
|
|
262
|
+
# Public entry point
|
|
263
|
+
# ---------------------------------------------------------------------------
|
|
264
|
+
|
|
265
|
+
def cmd_ops(args: Namespace) -> None:
|
|
266
|
+
"""Dispatch ``slm ops`` subcommands."""
|
|
267
|
+
sub = getattr(args, "ops_command", None)
|
|
268
|
+
handlers = {
|
|
269
|
+
"list": _cmd_ops_list,
|
|
270
|
+
"resolve": _cmd_ops_resolve,
|
|
271
|
+
"status": _cmd_ops_status,
|
|
272
|
+
}
|
|
273
|
+
handler = handlers.get(sub)
|
|
274
|
+
if handler:
|
|
275
|
+
handler(args)
|
|
276
|
+
else:
|
|
277
|
+
print("Usage: slm ops <list|resolve|status> [options]")
|
|
278
|
+
print(" slm ops list [--profile P] [--json]")
|
|
279
|
+
print(" slm ops resolve <id> --action {retry|force_reconcile|cancel} [--json]")
|
|
280
|
+
print(" slm ops status [--json]")
|
|
281
|
+
sys.exit(1)
|
|
@@ -410,7 +410,7 @@ def run_wizard(auto: bool = False) -> None:
|
|
|
410
410
|
|
|
411
411
|
print()
|
|
412
412
|
print("╔══════════════════════════════════════════════════════════╗")
|
|
413
|
-
print("║ SuperLocalMemory
|
|
413
|
+
print("║ SuperLocalMemory V4 — The Unified Brain ║")
|
|
414
414
|
print("║ by Varun Pratap Bhardwaj / Qualixar ║")
|
|
415
415
|
print("╚══════════════════════════════════════════════════════════╝")
|
|
416
416
|
print()
|