superlocalmemory 3.6.23 → 3.7.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/CHANGELOG.md +52 -0
- package/README.md +271 -71
- package/bin/slm-npm +43 -89
- package/ide/configs/antigravity-mcp.json +2 -2
- package/ide/configs/chatgpt-desktop-mcp.json +1 -1
- package/ide/configs/claude-desktop-mcp.json +2 -2
- package/ide/configs/windsurf-mcp.json +2 -2
- package/ide/hooks/context-hook.js +6 -2
- package/ide/hooks/post-recall-hook.js +7 -3
- package/ide/hooks/tool-event-hook.sh +2 -1
- package/package.json +18 -10
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/_GENERATED.md +1 -1
- package/plugin/agents/slm-memory-advisor.md +1 -1
- package/plugin/requirements.txt +1 -1
- package/plugin/skills/slm-session/SKILL.md +1 -1
- package/plugin-src/rules/AGENTS.md +1 -1
- package/pyproject.toml +40 -8
- package/scripts/postinstall-interactive.js +17 -94
- package/scripts/postinstall.js +185 -258
- package/scripts/preuninstall.js +9 -50
- package/src/superlocalmemory/__init__.py +2 -2
- package/src/superlocalmemory/attribution/mathematical_dna.py +1 -1
- package/src/superlocalmemory/attribution/signer.py +34 -19
- package/src/superlocalmemory/attribution/watermark.py +1 -1
- package/src/superlocalmemory/cli/_lazy_init.py +3 -5
- package/src/superlocalmemory/cli/commands.py +453 -191
- package/src/superlocalmemory/cli/context_commands.py +5 -4
- package/src/superlocalmemory/cli/daemon.py +282 -187
- package/src/superlocalmemory/cli/db_migrate.py +3 -1
- package/src/superlocalmemory/cli/diagnostics_cmd.py +28 -0
- package/src/superlocalmemory/cli/evidence_cmd.py +103 -0
- package/src/superlocalmemory/cli/ingest_cmd.py +7 -3
- package/src/superlocalmemory/cli/main.py +128 -31
- package/src/superlocalmemory/cli/pending_store.py +54 -38
- package/src/superlocalmemory/cli/scale_engine_cmd.py +37 -0
- package/src/superlocalmemory/cli/service_installer.py +57 -52
- package/src/superlocalmemory/cli/setup_wizard.py +142 -88
- package/src/superlocalmemory/cli/version_banner.py +2 -1
- package/src/superlocalmemory/code_graph/config.py +3 -1
- package/src/superlocalmemory/core/backend_orchestrator.py +81 -21
- package/src/superlocalmemory/core/config.py +65 -20
- package/src/superlocalmemory/core/consolidation_engine.py +9 -7
- package/src/superlocalmemory/core/context_cache.py +56 -8
- package/src/superlocalmemory/core/derivation_lineage.py +246 -0
- package/src/superlocalmemory/core/embedding_worker.py +32 -20
- package/src/superlocalmemory/core/embeddings.py +54 -18
- package/src/superlocalmemory/core/engine.py +150 -104
- package/src/superlocalmemory/core/engine_ingestion.py +513 -0
- package/src/superlocalmemory/core/engine_wiring.py +2 -0
- package/src/superlocalmemory/core/evidence_bundle.py +526 -0
- package/src/superlocalmemory/core/fact_consolidator.py +5 -11
- package/src/superlocalmemory/core/graph_analyzer.py +2 -2
- package/src/superlocalmemory/core/health_monitor.py +4 -2
- package/src/superlocalmemory/core/ingestion_command.py +636 -0
- package/src/superlocalmemory/core/injection.py +69 -18
- package/src/superlocalmemory/core/lifecycle_state.py +153 -0
- package/src/superlocalmemory/core/maintenance.py +1 -1
- package/src/superlocalmemory/core/maintenance_scheduler.py +51 -35
- package/src/superlocalmemory/core/mutations.py +143 -0
- package/src/superlocalmemory/core/platform_utils.py +7 -4
- package/src/superlocalmemory/core/ram_lock.py +16 -5
- package/src/superlocalmemory/core/rate_limit.py +1 -1
- package/src/superlocalmemory/core/recall_pipeline.py +60 -101
- package/src/superlocalmemory/core/recall_worker.py +76 -59
- package/src/superlocalmemory/core/registry.py +1 -1
- package/src/superlocalmemory/core/scale_engine.py +293 -0
- package/src/superlocalmemory/core/score_contract.py +62 -0
- package/src/superlocalmemory/core/security_primitives.py +3 -1
- package/src/superlocalmemory/core/slm_disabled.py +3 -5
- package/src/superlocalmemory/core/store_pipeline.py +172 -40
- package/src/superlocalmemory/core/tier_manager.py +32 -20
- package/src/superlocalmemory/core/worker_pool.py +13 -4
- package/src/superlocalmemory/dynamics/activation_guided_quantization.py +1 -1
- package/src/superlocalmemory/dynamics/eap_scheduler.py +10 -3
- package/src/superlocalmemory/dynamics/ebbinghaus_langevin_coupling.py +1 -1
- package/src/superlocalmemory/dynamics/fisher_langevin_coupling.py +1 -1
- package/src/superlocalmemory/encoding/auto_linker.py +1 -1
- package/src/superlocalmemory/encoding/cognitive_consolidator.py +7 -16
- package/src/superlocalmemory/encoding/consolidator.py +22 -5
- package/src/superlocalmemory/encoding/fact_extractor.py +1 -1
- package/src/superlocalmemory/encoding/foresight.py +2 -0
- package/src/superlocalmemory/encoding/graph_builder.py +1 -1
- package/src/superlocalmemory/encoding/temporal_parser.py +2 -0
- package/src/superlocalmemory/evaluation/__init__.py +13 -0
- package/src/superlocalmemory/evaluation/calibration.py +308 -0
- package/src/superlocalmemory/evolution/skill_evolver.py +2 -1
- package/src/superlocalmemory/graph/cozo_backend.py +256 -23
- package/src/superlocalmemory/hooks/_outcome_common.py +21 -11
- package/src/superlocalmemory/hooks/antigravity_adapter.py +10 -31
- package/src/superlocalmemory/hooks/auto_invoker.py +25 -27
- package/src/superlocalmemory/hooks/auto_recall.py +31 -6
- package/src/superlocalmemory/hooks/auto_recall_hook.py +13 -33
- package/src/superlocalmemory/hooks/before_web_hook.py +9 -7
- package/src/superlocalmemory/hooks/claude_code_hooks.py +123 -35
- package/src/superlocalmemory/hooks/codex_assets.py +59 -0
- package/src/superlocalmemory/hooks/codex_hooks.py +186 -0
- package/src/superlocalmemory/hooks/context_payload.py +1 -1
- package/src/superlocalmemory/hooks/copilot_adapter.py +9 -24
- package/src/superlocalmemory/hooks/cursor_adapter.py +10 -32
- package/src/superlocalmemory/hooks/hook_daemon.py +4 -2
- package/src/superlocalmemory/hooks/hook_handlers.py +219 -32
- package/src/superlocalmemory/hooks/memory_protocol.py +5 -3
- package/src/superlocalmemory/hooks/post_tool_async_hook.py +4 -2
- package/src/superlocalmemory/hooks/session_registry.py +15 -8
- package/src/superlocalmemory/hooks/stop_outcome_hook.py +10 -6
- package/src/superlocalmemory/hooks/topic_shift_hook.py +42 -12
- package/src/superlocalmemory/hooks/user_prompt_hook.py +9 -14
- package/src/superlocalmemory/hooks/user_prompt_rehash_hook.py +19 -11
- package/src/superlocalmemory/infra/auth_middleware.py +38 -5
- package/src/superlocalmemory/infra/backup.py +7 -5
- package/src/superlocalmemory/infra/cloud_backup.py +18 -8
- package/src/superlocalmemory/infra/daemon_identity.py +248 -0
- package/src/superlocalmemory/infra/data_root.py +199 -0
- package/src/superlocalmemory/infra/event_bus.py +3 -1
- package/src/superlocalmemory/infra/local_diagnostics.py +327 -0
- package/src/superlocalmemory/infra/process_reaper.py +23 -0
- package/src/superlocalmemory/ingestion/adapter_manager.py +27 -9
- package/src/superlocalmemory/ingestion/base_adapter.py +25 -31
- package/src/superlocalmemory/ingestion/calendar_adapter.py +13 -4
- package/src/superlocalmemory/ingestion/credentials.py +14 -7
- package/src/superlocalmemory/ingestion/gmail_adapter.py +13 -4
- package/src/superlocalmemory/ingestion/transcript_adapter.py +7 -2
- package/src/superlocalmemory/learning/consolidation_quantization_worker.py +1 -1
- package/src/superlocalmemory/learning/ensemble.py +11 -0
- package/src/superlocalmemory/learning/entity_compiler.py +1 -1
- package/src/superlocalmemory/learning/feedback.py +1 -1
- package/src/superlocalmemory/learning/forgetting_scheduler.py +12 -7
- package/src/superlocalmemory/learning/quantization_scheduler.py +1 -1
- package/src/superlocalmemory/learning/ranker.py +4 -1
- package/src/superlocalmemory/learning/source_quality.py +1 -1
- package/src/superlocalmemory/learning/trigram_index.py +3 -2
- package/src/superlocalmemory/llm/backbone.py +1 -1
- package/src/superlocalmemory/math/ebbinghaus.py +1 -1
- package/src/superlocalmemory/math/fisher.py +1 -1
- package/src/superlocalmemory/math/fisher_quantized.py +1 -1
- package/src/superlocalmemory/math/hopfield.py +1 -1
- package/src/superlocalmemory/math/langevin.py +1 -1
- package/src/superlocalmemory/math/polar_quant.py +3 -4
- package/src/superlocalmemory/math/qjl.py +1 -1
- package/src/superlocalmemory/math/sheaf.py +1 -1
- package/src/superlocalmemory/math/turbo_quant.py +3 -2
- package/src/superlocalmemory/mcp/_daemon_proxy.py +12 -11
- package/src/superlocalmemory/mcp/_pool_adapter.py +27 -0
- package/src/superlocalmemory/mcp/http_transport.py +53 -0
- package/src/superlocalmemory/mcp/server.py +39 -13
- package/src/superlocalmemory/mcp/shared.py +69 -3
- package/src/superlocalmemory/mcp/tools_active.py +141 -31
- package/src/superlocalmemory/mcp/tools_core.py +128 -29
- package/src/superlocalmemory/mcp/tools_evolution.py +5 -7
- package/src/superlocalmemory/mcp/tools_learning.py +42 -2
- package/src/superlocalmemory/mcp/tools_mesh.py +7 -23
- package/src/superlocalmemory/mcp/tools_optimize.py +8 -1
- package/src/superlocalmemory/mcp/tools_v28.py +23 -2
- package/src/superlocalmemory/mcp/tools_v3.py +26 -1
- package/src/superlocalmemory/mcp/tools_v33.py +56 -17
- package/src/superlocalmemory/mesh/broker.py +2 -0
- package/src/superlocalmemory/mesh/remote_sync.py +50 -12
- package/src/superlocalmemory/optimize/cache/manager.py +77 -1
- package/src/superlocalmemory/optimize/cache/semantic.py +23 -3
- package/src/superlocalmemory/optimize/compress/ccr.py +4 -0
- package/src/superlocalmemory/optimize/compress/router.py +6 -1
- package/src/superlocalmemory/optimize/config/__init__.py +5 -0
- package/src/superlocalmemory/optimize/config/store.py +6 -4
- package/src/superlocalmemory/optimize/proxy/_helpers.py +15 -5
- package/src/superlocalmemory/optimize/proxy/capture.py +3 -2
- package/src/superlocalmemory/optimize/proxy/server.py +2 -2
- package/src/superlocalmemory/optimize/storage/db.py +12 -12
- package/src/superlocalmemory/retrieval/agentic.py +1 -1
- package/src/superlocalmemory/retrieval/ann_index.py +1 -1
- package/src/superlocalmemory/retrieval/bm25_channel.py +35 -11
- package/src/superlocalmemory/retrieval/bridge_discovery.py +73 -8
- package/src/superlocalmemory/retrieval/engine.py +169 -79
- package/src/superlocalmemory/retrieval/entity_channel.py +289 -67
- package/src/superlocalmemory/retrieval/forgetting_filter.py +1 -1
- package/src/superlocalmemory/retrieval/fusion.py +1 -1
- package/src/superlocalmemory/retrieval/hopfield_channel.py +118 -30
- package/src/superlocalmemory/retrieval/profile_channel.py +1 -1
- package/src/superlocalmemory/retrieval/quantization_aware_search.py +16 -10
- package/src/superlocalmemory/retrieval/reranker.py +56 -20
- package/src/superlocalmemory/retrieval/scope_policy.py +85 -0
- package/src/superlocalmemory/retrieval/semantic_channel.py +122 -14
- package/src/superlocalmemory/retrieval/spreading_activation.py +141 -25
- package/src/superlocalmemory/retrieval/strategy.py +1 -1
- package/src/superlocalmemory/retrieval/temporal_channel.py +30 -15
- package/src/superlocalmemory/retrieval/vector_store.py +1 -1
- package/src/superlocalmemory/server/api.py +10 -7
- package/src/superlocalmemory/server/bandit_loops.py +4 -2
- package/src/superlocalmemory/server/recall_serializer.py +24 -0
- package/src/superlocalmemory/server/route_mutations.py +84 -0
- package/src/superlocalmemory/server/routes/agents.py +8 -6
- package/src/superlocalmemory/server/routes/brain.py +14 -12
- package/src/superlocalmemory/server/routes/chat.py +29 -12
- package/src/superlocalmemory/server/routes/data_io.py +55 -24
- package/src/superlocalmemory/server/routes/helpers.py +8 -63
- package/src/superlocalmemory/server/routes/ingest.py +53 -36
- package/src/superlocalmemory/server/routes/memories.py +104 -43
- package/src/superlocalmemory/server/routes/mesh.py +31 -0
- package/src/superlocalmemory/server/routes/profiles.py +26 -4
- package/src/superlocalmemory/server/routes/tiers.py +43 -11
- package/src/superlocalmemory/server/routes/timeline.py +5 -1
- package/src/superlocalmemory/server/routes/v3_api.py +76 -21
- package/src/superlocalmemory/server/security_middleware.py +1 -1
- package/src/superlocalmemory/server/unified_daemon.py +680 -293
- package/src/superlocalmemory/server/write_identity.py +147 -0
- package/src/superlocalmemory/storage/access_log.py +4 -3
- package/src/superlocalmemory/storage/database.py +118 -25
- package/src/superlocalmemory/storage/migration_runner.py +84 -1
- package/src/superlocalmemory/storage/migration_v33.py +1 -1
- package/src/superlocalmemory/storage/migrations/M002_model_state_history.py +6 -60
- package/src/superlocalmemory/storage/migrations/M018_ingestion_operations.py +120 -0
- package/src/superlocalmemory/storage/migrations/M019_derivation_lineage.py +54 -0
- package/src/superlocalmemory/storage/migrations/M020_model_state_integrity.py +52 -0
- package/src/superlocalmemory/storage/migrations/__init__.py +5 -0
- package/src/superlocalmemory/storage/models.py +16 -0
- package/src/superlocalmemory/storage/quantized_store.py +20 -3
- package/src/superlocalmemory/storage/v2_migrator.py +5 -3
- package/src/superlocalmemory/ui/favicon.svg +5 -0
- package/src/superlocalmemory/ui/index.html +1 -0
- package/src/superlocalmemory/ui/js/compliance.js +1 -1
- package/src/superlocalmemory/ui/js/core.js +49 -8
- package/src/superlocalmemory/ui/js/dashboard.js +23 -2
- package/src/superlocalmemory/ui/js/feedback.js +1 -1
- package/src/superlocalmemory/ui/js/graph-filters.js +1 -1
- package/src/superlocalmemory/ui/js/graph-ui.js +1 -1
- package/src/superlocalmemory/ui/js/lifecycle.js +1 -1
- package/src/superlocalmemory/ui/js/ng-mesh.js +15 -49
- package/src/superlocalmemory/ui/js/settings.js +4 -2
- package/src/superlocalmemory/vector/lancedb_backend.py +57 -9
- package/bin/slm +0 -59
- package/bin/slm.bat +0 -77
- package/bin/slm.cmd +0 -5
- package/ide/integrations/langchain/README.md +0 -106
- package/ide/integrations/langchain/langchain_superlocalmemory/__init__.py +0 -9
- package/ide/integrations/langchain/langchain_superlocalmemory/chat_message_history.py +0 -201
- package/ide/integrations/langchain/pyproject.toml +0 -38
- package/ide/integrations/langchain/tests/__init__.py +0 -3
- package/ide/integrations/langchain/tests/test_chat_message_history.py +0 -215
- package/ide/integrations/langchain/tests/test_security.py +0 -117
- package/ide/integrations/llamaindex/README.md +0 -81
- package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/__init__.py +0 -9
- package/ide/integrations/llamaindex/llama_index/storage/chat_store/superlocalmemory/base.py +0 -316
- package/ide/integrations/llamaindex/pyproject.toml +0 -43
- package/ide/integrations/llamaindex/tests/__init__.py +0 -3
- package/ide/integrations/llamaindex/tests/test_chat_store.py +0 -294
- package/ide/integrations/llamaindex/tests/test_security.py +0 -241
- 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 -15
- package/plugin-src/scripts/slm-launch.bat +0 -17
- package/plugin-src/settings.json +0 -16
- package/plugin-src/skills/slm-cache/SKILL.md +0 -140
- package/plugin-src/skills/slm-compress/SKILL.md +0 -143
- package/plugin-src/skills/slm-graph/SKILL.md +0 -300
- package/plugin-src/skills/slm-recall/SKILL.md +0 -204
- package/plugin-src/skills/slm-remember/SKILL.md +0 -194
- package/plugin-src/skills/slm-session/SKILL.md +0 -207
- package/plugin-src/skills/slm-status/SKILL.md +0 -149
- package/scripts/__tests__/build-plugin.test.mjs +0 -613
- package/scripts/_savings_math.py +0 -270
- package/scripts/build-dmg.sh +0 -417
- package/scripts/build-plugin.js +0 -742
- package/scripts/build-slm-hook.ps1 +0 -40
- package/scripts/build-slm-hook.sh +0 -45
- package/scripts/build_entry.py +0 -452
- package/scripts/ci/stage5b_gate.sh +0 -50
- package/scripts/dogfood_savings.py +0 -490
- package/scripts/generate-thumbnails.py +0 -218
- package/scripts/install-skills.ps1 +0 -4
- package/scripts/install-skills.sh +0 -5
- package/scripts/install.ps1 +0 -701
- package/scripts/install.sh +0 -1015
- package/scripts/postinstall_binary.js +0 -287
- package/scripts/prepack.js +0 -33
- package/scripts/release_manifest.py +0 -273
- package/scripts/slm-hook.spec +0 -56
- package/scripts/start-dashboard.ps1 +0 -52
- package/scripts/start-dashboard.sh +0 -41
- package/scripts/sync-wiki.ps1 +0 -127
- package/scripts/sync-wiki.sh +0 -82
- package/scripts/test-dmg.sh +0 -161
- package/scripts/test-npm-package.ps1 +0 -252
- package/scripts/test-npm-package.sh +0 -207
- package/scripts/verify-install.ps1 +0 -294
- package/scripts/verify-install.sh +0 -266
- package/scripts/verify-v27.ps1 +0 -301
- package/scripts/verify-v27.sh +0 -233
- package/src/superlocalmemory.egg-info/PKG-INFO +0 -516
- package/src/superlocalmemory.egg-info/SOURCES.txt +0 -529
- package/src/superlocalmemory.egg-info/dependency_links.txt +0 -1
- package/src/superlocalmemory.egg-info/entry_points.txt +0 -2
- package/src/superlocalmemory.egg-info/requires.txt +0 -71
- package/src/superlocalmemory.egg-info/top_level.txt +0 -1
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
+
|
|
4
|
+
"""Production adapter between MemoryEngine and canonical ingestion.
|
|
5
|
+
|
|
6
|
+
This is the anti-corruption layer for the expand-migrate-contract rollout:
|
|
7
|
+
the command owns durability and state, while the existing engine remains the
|
|
8
|
+
implementation of queryable projection and complete derivation.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import hashlib
|
|
14
|
+
import uuid
|
|
15
|
+
from typing import TYPE_CHECKING
|
|
16
|
+
|
|
17
|
+
from superlocalmemory.core.ingestion_command import (
|
|
18
|
+
IngestionCommand,
|
|
19
|
+
IngestionOperation,
|
|
20
|
+
IngestionOperationRepository,
|
|
21
|
+
IngestionRequest,
|
|
22
|
+
MaterializationResult,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from superlocalmemory.core.engine import MemoryEngine
|
|
27
|
+
from superlocalmemory.storage.models import AtomicFact
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
_PREBUILT_FACT_KEY = "_slm_prebuilt_fact_v1"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def content_passes_admission(content: str) -> bool:
|
|
34
|
+
"""Return whether raw content is eligible to reach durable ingestion.
|
|
35
|
+
|
|
36
|
+
This is deliberately stateless: canonical admission must not populate the
|
|
37
|
+
live entropy gate's duplicate window before the materialization pipeline
|
|
38
|
+
evaluates the same content.
|
|
39
|
+
"""
|
|
40
|
+
if not content or not content.strip():
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
from superlocalmemory.core.injection import is_low_quality
|
|
44
|
+
from superlocalmemory.encoding.entropy_gate import EntropyGate
|
|
45
|
+
|
|
46
|
+
return not is_low_quality(content) and EntropyGate().should_pass(content)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _prebuilt_fact_payload(fact: AtomicFact) -> dict:
|
|
50
|
+
"""Serialize the stable public AtomicFact contract into M018 metadata."""
|
|
51
|
+
enum_fields = {"fact_type", "lifecycle", "signal_type"}
|
|
52
|
+
fields = (
|
|
53
|
+
"fact_id", "memory_id", "profile_id", "scope", "shared_with",
|
|
54
|
+
"content", "fact_type", "entities", "canonical_entities",
|
|
55
|
+
"observation_date", "referenced_date", "interval_start", "interval_end",
|
|
56
|
+
"confidence", "importance", "evidence_count", "access_count",
|
|
57
|
+
"source_turn_ids", "session_id", "embedding", "fisher_mean",
|
|
58
|
+
"fisher_variance", "lifecycle", "langevin_position",
|
|
59
|
+
"emotional_valence", "emotional_arousal", "signal_type", "pinned",
|
|
60
|
+
"created_at",
|
|
61
|
+
)
|
|
62
|
+
payload = {}
|
|
63
|
+
for name in fields:
|
|
64
|
+
value = getattr(fact, name)
|
|
65
|
+
payload[name] = value.value if name in enum_fields else value
|
|
66
|
+
return payload
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _prebuilt_fact_from_payload(payload: dict):
|
|
70
|
+
from superlocalmemory.storage.models import (
|
|
71
|
+
AtomicFact,
|
|
72
|
+
FactType,
|
|
73
|
+
MemoryLifecycle,
|
|
74
|
+
SignalType,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
values = dict(payload)
|
|
78
|
+
values["fact_type"] = FactType(values.get("fact_type", FactType.SEMANTIC.value))
|
|
79
|
+
values["lifecycle"] = MemoryLifecycle(
|
|
80
|
+
values.get("lifecycle", MemoryLifecycle.ACTIVE.value)
|
|
81
|
+
)
|
|
82
|
+
values["signal_type"] = SignalType(
|
|
83
|
+
values.get("signal_type", SignalType.FACTUAL.value)
|
|
84
|
+
)
|
|
85
|
+
return AtomicFact(**values)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def local_trusted_actor_id(actor_kind: str) -> str:
|
|
89
|
+
"""Derive a stable local actor from the private install capability."""
|
|
90
|
+
from superlocalmemory.core.security_primitives import ensure_install_token
|
|
91
|
+
from superlocalmemory.infra.daemon_identity import namespace_id_for, owner_id
|
|
92
|
+
from superlocalmemory.infra.data_root import canonical_data_root
|
|
93
|
+
|
|
94
|
+
token = ensure_install_token()
|
|
95
|
+
material = (
|
|
96
|
+
b"superlocalmemory-local-actor-v1\0"
|
|
97
|
+
+ token.encode("utf-8")
|
|
98
|
+
+ b"\0"
|
|
99
|
+
+ actor_kind.encode("utf-8")
|
|
100
|
+
)
|
|
101
|
+
fingerprint = hashlib.sha256(material).hexdigest()
|
|
102
|
+
return (
|
|
103
|
+
f"local-capability:{actor_kind}:{owner_id()}:"
|
|
104
|
+
f"{namespace_id_for(canonical_data_root())}:{fingerprint}"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def canonical_store(
|
|
109
|
+
engine: MemoryEngine,
|
|
110
|
+
content: str,
|
|
111
|
+
*,
|
|
112
|
+
source_type: str,
|
|
113
|
+
trusted_actor_id: str,
|
|
114
|
+
metadata: dict | None = None,
|
|
115
|
+
scope: str = "personal",
|
|
116
|
+
shared_with: list[str] | tuple[str, ...] | None = None,
|
|
117
|
+
session_id: str = "",
|
|
118
|
+
session_date: str | None = None,
|
|
119
|
+
speaker: str = "",
|
|
120
|
+
role: str = "user",
|
|
121
|
+
idempotency_key: str = "",
|
|
122
|
+
require_complete: bool = True,
|
|
123
|
+
return_receipt: bool = False,
|
|
124
|
+
) -> list[str] | IngestionOperation:
|
|
125
|
+
"""Synchronously submit and completely materialize one canonical write."""
|
|
126
|
+
import time
|
|
127
|
+
|
|
128
|
+
from superlocalmemory.core.ingestion_command import IngestionRequest, IngestionState
|
|
129
|
+
from superlocalmemory.infra.local_diagnostics import record_operation
|
|
130
|
+
|
|
131
|
+
started = time.monotonic()
|
|
132
|
+
|
|
133
|
+
# Preserve the long-standing Python/API contract for rejected content:
|
|
134
|
+
# low-information input is a no-op and never becomes an M018 operation.
|
|
135
|
+
if not content_passes_admission(content):
|
|
136
|
+
record_operation(
|
|
137
|
+
"remember",
|
|
138
|
+
client=trusted_actor_id,
|
|
139
|
+
duration_ms=(time.monotonic() - started) * 1000.0,
|
|
140
|
+
error=ValueError("content rejected by local admission policy"),
|
|
141
|
+
)
|
|
142
|
+
return []
|
|
143
|
+
try:
|
|
144
|
+
command = build_engine_ingestion_command(engine)
|
|
145
|
+
receipt = command.submit(IngestionRequest(
|
|
146
|
+
content=content,
|
|
147
|
+
profile_id=engine._profile_id,
|
|
148
|
+
source_type=source_type,
|
|
149
|
+
idempotency_key=idempotency_key or uuid.uuid4().hex,
|
|
150
|
+
metadata=dict(metadata or {}),
|
|
151
|
+
scope=scope,
|
|
152
|
+
shared_with=tuple(shared_with or ()),
|
|
153
|
+
trusted_actor_id=trusted_actor_id,
|
|
154
|
+
session_id=session_id,
|
|
155
|
+
session_date=session_date or "",
|
|
156
|
+
speaker=speaker,
|
|
157
|
+
role=role,
|
|
158
|
+
))
|
|
159
|
+
result = command.materialize(receipt.operation_id)
|
|
160
|
+
if result.state is not IngestionState.COMPLETE:
|
|
161
|
+
if not require_complete and receipt.fact_ids:
|
|
162
|
+
import logging
|
|
163
|
+
logging.getLogger(__name__).warning(
|
|
164
|
+
"Canonical operation %s remains %s and will retry: %s",
|
|
165
|
+
result.operation_id,
|
|
166
|
+
result.state.value,
|
|
167
|
+
result.last_error,
|
|
168
|
+
)
|
|
169
|
+
record_operation(
|
|
170
|
+
"remember", client=trusted_actor_id,
|
|
171
|
+
duration_ms=(time.monotonic() - started) * 1000.0,
|
|
172
|
+
)
|
|
173
|
+
return list(receipt.fact_ids)
|
|
174
|
+
raise RuntimeError(result.last_error or "canonical materialization failed")
|
|
175
|
+
except Exception as exc:
|
|
176
|
+
record_operation(
|
|
177
|
+
"remember",
|
|
178
|
+
client=trusted_actor_id,
|
|
179
|
+
duration_ms=(time.monotonic() - started) * 1000.0,
|
|
180
|
+
error=exc,
|
|
181
|
+
)
|
|
182
|
+
raise
|
|
183
|
+
record_operation(
|
|
184
|
+
"remember",
|
|
185
|
+
client=trusted_actor_id,
|
|
186
|
+
duration_ms=(time.monotonic() - started) * 1000.0,
|
|
187
|
+
)
|
|
188
|
+
return result if return_receipt else list(result.fact_ids)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def canonical_store_fn(
|
|
192
|
+
engine: MemoryEngine,
|
|
193
|
+
*,
|
|
194
|
+
source_type: str,
|
|
195
|
+
trusted_actor_id: str,
|
|
196
|
+
):
|
|
197
|
+
"""Return an AutoCapture-compatible synchronous canonical store callable."""
|
|
198
|
+
def store(content: str, metadata: dict | None = None) -> list[str]:
|
|
199
|
+
values = dict(metadata or {})
|
|
200
|
+
return canonical_store(
|
|
201
|
+
engine,
|
|
202
|
+
content,
|
|
203
|
+
source_type=source_type,
|
|
204
|
+
trusted_actor_id=trusted_actor_id,
|
|
205
|
+
metadata=values,
|
|
206
|
+
session_id=str(values.get("session_id") or ""),
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
return store
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def canonical_store_fact(
|
|
213
|
+
engine: MemoryEngine,
|
|
214
|
+
fact: AtomicFact,
|
|
215
|
+
*,
|
|
216
|
+
trusted_actor_id: str,
|
|
217
|
+
) -> str:
|
|
218
|
+
"""Durably ingest a caller-built fact without changing its public ID."""
|
|
219
|
+
from superlocalmemory.core.ingestion_command import IngestionRequest, IngestionState
|
|
220
|
+
from superlocalmemory.core.injection import is_low_quality
|
|
221
|
+
|
|
222
|
+
if is_low_quality(fact.content):
|
|
223
|
+
return fact.fact_id
|
|
224
|
+
command = build_engine_ingestion_command(engine)
|
|
225
|
+
receipt = command.submit(IngestionRequest(
|
|
226
|
+
content=fact.content,
|
|
227
|
+
profile_id=engine._profile_id,
|
|
228
|
+
source_type="python-api-prebuilt",
|
|
229
|
+
idempotency_key=f"prebuilt:{fact.fact_id}",
|
|
230
|
+
metadata={_PREBUILT_FACT_KEY: _prebuilt_fact_payload(fact)},
|
|
231
|
+
scope=fact.scope or "personal",
|
|
232
|
+
shared_with=tuple(fact.shared_with or ()),
|
|
233
|
+
trusted_actor_id=trusted_actor_id,
|
|
234
|
+
session_id=fact.session_id,
|
|
235
|
+
session_date=fact.observation_date or "",
|
|
236
|
+
))
|
|
237
|
+
result = command.materialize(receipt.operation_id)
|
|
238
|
+
if result.state is not IngestionState.COMPLETE:
|
|
239
|
+
raise RuntimeError(result.last_error or "prebuilt fact materialization failed")
|
|
240
|
+
if fact.fact_id not in result.fact_ids:
|
|
241
|
+
raise RuntimeError("prebuilt fact ID was not preserved by canonical ingestion")
|
|
242
|
+
stored = engine._db.get_fact(fact.fact_id)
|
|
243
|
+
if stored is not None:
|
|
244
|
+
fact.memory_id = stored.memory_id
|
|
245
|
+
return fact.fact_id
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def build_engine_ingestion_command(engine: MemoryEngine) -> IngestionCommand:
|
|
249
|
+
"""Bind one initialized engine to the durable ingestion command."""
|
|
250
|
+
engine._require_full("canonical_ingestion")
|
|
251
|
+
engine._ensure_init()
|
|
252
|
+
repository = IngestionOperationRepository(engine._db)
|
|
253
|
+
|
|
254
|
+
def write_queryable(request: IngestionRequest, operation_id: str) -> list[str]:
|
|
255
|
+
if request.profile_id != engine._profile_id:
|
|
256
|
+
raise ValueError("ingestion request profile does not match engine")
|
|
257
|
+
if not request.trusted_actor_id:
|
|
258
|
+
raise ValueError("trusted actor identity is required")
|
|
259
|
+
hook_context = {
|
|
260
|
+
"operation": "store",
|
|
261
|
+
"agent_id": request.trusted_actor_id or "unknown",
|
|
262
|
+
"profile_id": request.profile_id,
|
|
263
|
+
"content_preview": request.content[:100],
|
|
264
|
+
"ingestion_operation_id": operation_id,
|
|
265
|
+
}
|
|
266
|
+
# Authorization and trust policy must run before raw evidence reaches
|
|
267
|
+
# durable storage. Materialization reuses this authorization decision.
|
|
268
|
+
engine._hooks.run_pre("store", hook_context)
|
|
269
|
+
metadata = dict(request.metadata)
|
|
270
|
+
metadata["ingestion_operation_id"] = operation_id
|
|
271
|
+
if request.session_id:
|
|
272
|
+
metadata.setdefault("session_id", request.session_id)
|
|
273
|
+
prebuilt_payload = metadata.get(_PREBUILT_FACT_KEY)
|
|
274
|
+
if isinstance(prebuilt_payload, dict):
|
|
275
|
+
from superlocalmemory.storage.models import MemoryRecord
|
|
276
|
+
|
|
277
|
+
fact = _prebuilt_fact_from_payload(prebuilt_payload)
|
|
278
|
+
fact.profile_id = request.profile_id
|
|
279
|
+
fact.scope = request.scope
|
|
280
|
+
fact.shared_with = list(request.shared_with) or None
|
|
281
|
+
fact.session_id = request.session_id or fact.session_id
|
|
282
|
+
if request.session_date:
|
|
283
|
+
fact.observation_date = request.session_date
|
|
284
|
+
memory_id = fact.memory_id
|
|
285
|
+
memory_rows = (
|
|
286
|
+
engine._db.execute(
|
|
287
|
+
"SELECT memory_id FROM memories WHERE memory_id=? AND profile_id=?",
|
|
288
|
+
(memory_id, request.profile_id),
|
|
289
|
+
)
|
|
290
|
+
if memory_id else []
|
|
291
|
+
)
|
|
292
|
+
if not memory_rows:
|
|
293
|
+
record = MemoryRecord(
|
|
294
|
+
memory_id=memory_id or uuid.uuid4().hex,
|
|
295
|
+
profile_id=request.profile_id,
|
|
296
|
+
content=request.content,
|
|
297
|
+
session_id=request.session_id,
|
|
298
|
+
session_date=request.session_date,
|
|
299
|
+
metadata=metadata,
|
|
300
|
+
scope=request.scope,
|
|
301
|
+
shared_with=list(request.shared_with) or None,
|
|
302
|
+
)
|
|
303
|
+
engine._db.store_memory(record)
|
|
304
|
+
memory_id = record.memory_id
|
|
305
|
+
fact.memory_id = memory_id
|
|
306
|
+
engine._db.store_fact(fact)
|
|
307
|
+
return [fact.fact_id]
|
|
308
|
+
return engine.store_fast(
|
|
309
|
+
request.content,
|
|
310
|
+
metadata=metadata,
|
|
311
|
+
scope=request.scope,
|
|
312
|
+
shared_with=list(request.shared_with) or None,
|
|
313
|
+
session_date=request.session_date or None,
|
|
314
|
+
speaker=request.speaker,
|
|
315
|
+
role=request.role,
|
|
316
|
+
index_external=False,
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
def materialize(operation: IngestionOperation) -> MaterializationResult:
|
|
320
|
+
facts = engine._db.get_facts_by_ids(
|
|
321
|
+
list(operation.queryable_fact_ids),
|
|
322
|
+
operation.profile_id,
|
|
323
|
+
)
|
|
324
|
+
if len(facts) != len(operation.queryable_fact_ids):
|
|
325
|
+
raise ValueError("queryable fact profile mismatch or missing fact")
|
|
326
|
+
memory_ids = {fact.memory_id for fact in facts}
|
|
327
|
+
if len(memory_ids) != 1:
|
|
328
|
+
raise ValueError("queryable facts do not share one source memory")
|
|
329
|
+
memory_id = next(iter(memory_ids))
|
|
330
|
+
|
|
331
|
+
from superlocalmemory.core.store_pipeline import run_store
|
|
332
|
+
|
|
333
|
+
is_prebuilt = isinstance(operation.metadata.get(_PREBUILT_FACT_KEY), dict)
|
|
334
|
+
|
|
335
|
+
class _QueryableProjectionExtractor:
|
|
336
|
+
@staticmethod
|
|
337
|
+
def extract_facts(**_kwargs):
|
|
338
|
+
return []
|
|
339
|
+
|
|
340
|
+
pipeline_state: dict[str, bool] = {}
|
|
341
|
+
fact_ids = run_store(
|
|
342
|
+
operation.raw_content,
|
|
343
|
+
operation.profile_id,
|
|
344
|
+
session_id=operation.session_id,
|
|
345
|
+
session_date=operation.session_date or None,
|
|
346
|
+
speaker=operation.speaker,
|
|
347
|
+
role=operation.role,
|
|
348
|
+
metadata=dict(operation.metadata),
|
|
349
|
+
scope=operation.scope,
|
|
350
|
+
shared_with=list(operation.shared_with) or None,
|
|
351
|
+
config=engine._config,
|
|
352
|
+
db=engine._db,
|
|
353
|
+
embedder=engine._embedder,
|
|
354
|
+
fact_extractor=(
|
|
355
|
+
_QueryableProjectionExtractor() if is_prebuilt else engine._fact_extractor
|
|
356
|
+
),
|
|
357
|
+
entity_resolver=engine._entity_resolver,
|
|
358
|
+
temporal_parser=engine._temporal_parser,
|
|
359
|
+
type_router=None if is_prebuilt else engine._type_router,
|
|
360
|
+
graph_builder=engine._graph_builder,
|
|
361
|
+
consolidator=None if is_prebuilt else engine._consolidator,
|
|
362
|
+
observation_builder=engine._observation_builder,
|
|
363
|
+
scene_builder=engine._scene_builder,
|
|
364
|
+
entropy_gate=engine._entropy_gate,
|
|
365
|
+
# External indexes are projected only after the relational unit of
|
|
366
|
+
# work commits; sqlite-vec uses a separate connection.
|
|
367
|
+
ann_index=None,
|
|
368
|
+
sheaf_checker=engine._sheaf_checker,
|
|
369
|
+
retrieval_engine=None,
|
|
370
|
+
provenance=engine._provenance,
|
|
371
|
+
hooks=engine._hooks,
|
|
372
|
+
vector_store=None,
|
|
373
|
+
context_generator=engine._context_generator,
|
|
374
|
+
temporal_validator=engine._temporal_validator,
|
|
375
|
+
auto_linker=engine._auto_linker,
|
|
376
|
+
consolidation_engine=engine._consolidation_engine,
|
|
377
|
+
existing_memory_id=memory_id,
|
|
378
|
+
queryable_fact_ids=operation.queryable_fact_ids,
|
|
379
|
+
trusted_actor_id=operation.trusted_actor_id,
|
|
380
|
+
pre_authorized=True,
|
|
381
|
+
ingestion_source_type=operation.source_type,
|
|
382
|
+
ingestion_operation_id=operation.operation_id,
|
|
383
|
+
derivation_report=pipeline_state,
|
|
384
|
+
)
|
|
385
|
+
if not fact_ids:
|
|
386
|
+
return MaterializationResult((), {"relational": False})
|
|
387
|
+
if is_prebuilt:
|
|
388
|
+
pipeline_state["extraction"] = True
|
|
389
|
+
pipeline_state["consolidation"] = True
|
|
390
|
+
|
|
391
|
+
placeholders = ",".join("?" for _ in fact_ids)
|
|
392
|
+
relational_count = engine._db.execute(
|
|
393
|
+
f"SELECT COUNT(*) AS count FROM atomic_facts "
|
|
394
|
+
f"WHERE fact_id IN ({placeholders}) AND profile_id=?",
|
|
395
|
+
(*fact_ids, operation.profile_id),
|
|
396
|
+
)
|
|
397
|
+
fts_count = engine._db.execute(
|
|
398
|
+
f"SELECT COUNT(*) AS count FROM atomic_facts_fts "
|
|
399
|
+
f"WHERE fact_id IN ({placeholders})",
|
|
400
|
+
tuple(fact_ids),
|
|
401
|
+
)
|
|
402
|
+
provenance_count = engine._db.execute(
|
|
403
|
+
f"SELECT COUNT(DISTINCT fact_id) AS count FROM provenance "
|
|
404
|
+
f"WHERE fact_id IN ({placeholders}) AND profile_id=? "
|
|
405
|
+
"AND source_type=? AND source_id=? AND created_by=?",
|
|
406
|
+
(
|
|
407
|
+
*fact_ids,
|
|
408
|
+
operation.profile_id,
|
|
409
|
+
operation.source_type,
|
|
410
|
+
operation.operation_id,
|
|
411
|
+
operation.trusted_actor_id,
|
|
412
|
+
),
|
|
413
|
+
)
|
|
414
|
+
temporal_required = engine._temporal_validator is not None
|
|
415
|
+
temporal_complete = True
|
|
416
|
+
if temporal_required:
|
|
417
|
+
temporal_count = engine._db.execute(
|
|
418
|
+
f"SELECT COUNT(DISTINCT fact_id) AS count FROM fact_temporal_validity "
|
|
419
|
+
f"WHERE fact_id IN ({placeholders}) AND profile_id=?",
|
|
420
|
+
(*fact_ids, operation.profile_id),
|
|
421
|
+
)
|
|
422
|
+
temporal_complete = int(dict(temporal_count[0])["count"]) == len(fact_ids)
|
|
423
|
+
|
|
424
|
+
embeddings_complete = True
|
|
425
|
+
if engine._embedder is not None:
|
|
426
|
+
embedding_count = engine._db.execute(
|
|
427
|
+
f"SELECT COUNT(*) AS count FROM atomic_facts "
|
|
428
|
+
f"WHERE fact_id IN ({placeholders}) AND embedding IS NOT NULL",
|
|
429
|
+
tuple(fact_ids),
|
|
430
|
+
)
|
|
431
|
+
embeddings_complete = (
|
|
432
|
+
int(dict(embedding_count[0])["count"]) == len(fact_ids)
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
derivation_state = {
|
|
436
|
+
"relational": int(dict(relational_count[0])["count"]) == len(fact_ids),
|
|
437
|
+
"fts": int(dict(fts_count[0])["count"]) == len(fact_ids),
|
|
438
|
+
"extraction": pipeline_state.get("extraction", False),
|
|
439
|
+
"canonicalization": pipeline_state.get("canonicalization", False),
|
|
440
|
+
"consolidation": pipeline_state.get("consolidation", False),
|
|
441
|
+
"graph": pipeline_state.get("graph", False),
|
|
442
|
+
"temporal": (
|
|
443
|
+
temporal_complete and pipeline_state.get("temporal", False)
|
|
444
|
+
),
|
|
445
|
+
"provenance": (
|
|
446
|
+
int(dict(provenance_count[0])["count"]) == len(fact_ids)
|
|
447
|
+
and pipeline_state.get("provenance", False)
|
|
448
|
+
),
|
|
449
|
+
"trust_policy": bool(operation.trusted_actor_id),
|
|
450
|
+
"embeddings": embeddings_complete,
|
|
451
|
+
}
|
|
452
|
+
return MaterializationResult(tuple(fact_ids), derivation_state)
|
|
453
|
+
|
|
454
|
+
def project(operation: IngestionOperation) -> dict[str, bool]:
|
|
455
|
+
facts = engine._db.get_facts_by_ids(
|
|
456
|
+
list(operation.final_fact_ids),
|
|
457
|
+
operation.profile_id,
|
|
458
|
+
)
|
|
459
|
+
if len(facts) != len(operation.final_fact_ids):
|
|
460
|
+
raise RuntimeError("external projection facts are missing")
|
|
461
|
+
|
|
462
|
+
ann_required = engine._ann_index is not None
|
|
463
|
+
vector_store = engine._vector_store
|
|
464
|
+
vector_required = bool(
|
|
465
|
+
vector_store is not None and getattr(vector_store, "available", False)
|
|
466
|
+
)
|
|
467
|
+
ann_complete = True
|
|
468
|
+
vector_complete = True
|
|
469
|
+
bm25_complete = True
|
|
470
|
+
bm25 = getattr(engine._retrieval_engine, "_bm25", None)
|
|
471
|
+
for fact in facts:
|
|
472
|
+
if not fact.embedding:
|
|
473
|
+
if ann_required:
|
|
474
|
+
ann_complete = False
|
|
475
|
+
if vector_required:
|
|
476
|
+
vector_complete = False
|
|
477
|
+
if fact.embedding and ann_required:
|
|
478
|
+
try:
|
|
479
|
+
engine._ann_index.add(fact.fact_id, fact.embedding)
|
|
480
|
+
except Exception:
|
|
481
|
+
ann_complete = False
|
|
482
|
+
if fact.embedding and vector_required:
|
|
483
|
+
if not vector_store.upsert(
|
|
484
|
+
fact_id=fact.fact_id,
|
|
485
|
+
profile_id=operation.profile_id,
|
|
486
|
+
embedding=fact.embedding,
|
|
487
|
+
):
|
|
488
|
+
vector_complete = False
|
|
489
|
+
if bm25 is not None:
|
|
490
|
+
try:
|
|
491
|
+
bm25.add(fact.fact_id, fact.content, operation.profile_id)
|
|
492
|
+
except Exception:
|
|
493
|
+
bm25_complete = False
|
|
494
|
+
return {
|
|
495
|
+
"ann": ann_complete,
|
|
496
|
+
"vector": vector_complete,
|
|
497
|
+
"bm25": bm25_complete,
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
return IngestionCommand(
|
|
501
|
+
repository,
|
|
502
|
+
write_queryable=write_queryable,
|
|
503
|
+
materialize=materialize,
|
|
504
|
+
project=project,
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
__all__ = [
|
|
509
|
+
"build_engine_ingestion_command",
|
|
510
|
+
"canonical_store",
|
|
511
|
+
"canonical_store_fn",
|
|
512
|
+
"local_trusted_actor_id",
|
|
513
|
+
]
|
|
@@ -613,6 +613,8 @@ def wire_hooks(
|
|
|
613
613
|
ctx.get("agent_id", "unknown"), ctx.get("profile_id", profile_id)))
|
|
614
614
|
hooks.register_pre("delete", lambda ctx: gate.check_delete(
|
|
615
615
|
ctx.get("agent_id", "unknown"), ctx.get("profile_id", profile_id)))
|
|
616
|
+
hooks.register_pre("update", lambda ctx: gate.check_write(
|
|
617
|
+
ctx.get("agent_id", "unknown"), ctx.get("profile_id", profile_id)))
|
|
616
618
|
|
|
617
619
|
# -- Post-store hooks (async, never block) --
|
|
618
620
|
if trust_scorer:
|