superlocalmemory 3.7.8 → 3.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/ATTRIBUTION.md +1 -3
- package/CHANGELOG.md +129 -0
- package/README.md +205 -123
- package/package.json +12 -3
- package/plugin/.claude-plugin/plugin.json +2 -3
- package/plugin/CLAUDE.md +8 -8
- package/plugin/agents/slm-governance-advisor.md +80 -0
- package/plugin/agents/slm-loop-runner.md +71 -0
- package/plugin/agents/slm-memory-advisor.md +10 -5
- package/plugin/agents/slm-optimize-advisor.md +9 -3
- package/plugin/commands/slm-loop.md +31 -0
- package/plugin/hooks/hooks.json +79 -0
- package/plugin/requirements.txt +1 -1
- package/plugin/scripts/slm-launch +46 -7
- package/plugin/settings.json +9 -0
- package/plugin/skills/slm-cache/SKILL.md +9 -1
- package/plugin/skills/slm-compress/SKILL.md +8 -1
- package/plugin/skills/slm-governance/SKILL.md +248 -0
- package/plugin/skills/slm-graph/SKILL.md +17 -3
- package/plugin/skills/slm-loop/SKILL.md +99 -0
- package/plugin/skills/slm-mesh/SKILL.md +282 -0
- package/plugin/skills/slm-profile/SKILL.md +148 -0
- package/plugin/skills/slm-recall/SKILL.md +46 -10
- package/plugin/skills/slm-remember/SKILL.md +48 -1
- package/plugin/skills/slm-scope/SKILL.md +176 -0
- package/plugin/skills/slm-session/SKILL.md +24 -1
- package/plugin/skills/slm-status/SKILL.md +18 -1
- package/plugin-src/rules/AGENTS.md +57 -18
- package/plugin-src/skills/slm-cache/SKILL.md +9 -1
- package/plugin-src/skills/slm-compress/SKILL.md +8 -1
- package/plugin-src/skills/slm-graph/SKILL.md +17 -3
- package/plugin-src/skills/slm-recall/SKILL.md +46 -10
- package/plugin-src/skills/slm-remember/SKILL.md +48 -1
- package/plugin-src/skills/slm-session/SKILL.md +24 -1
- package/plugin-src/skills/slm-status/SKILL.md +18 -1
- package/pyproject.toml +2 -1
- package/scripts/postinstall/validation.js +2 -0
- package/scripts/postinstall-interactive.js +74 -2
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/access/__init__.py +3 -0
- package/src/superlocalmemory/access/rbac.py +477 -0
- package/src/superlocalmemory/cli/commands.py +228 -17
- package/src/superlocalmemory/cli/compress_cmd.py +17 -7
- package/src/superlocalmemory/cli/daemon.py +7 -0
- package/src/superlocalmemory/cli/loop_cmd.py +187 -0
- package/src/superlocalmemory/cli/main.py +49 -8
- package/src/superlocalmemory/cli/mesh_cmd.py +38 -0
- package/src/superlocalmemory/cli/optimize_cmd.py +3 -0
- package/src/superlocalmemory/cli/pending_store.py +49 -13
- package/src/superlocalmemory/cli/proxy_cmd.py +4 -0
- package/src/superlocalmemory/cli/scale_engine_cmd.py +6 -0
- package/src/superlocalmemory/cli/setup_wizard.py +22 -13
- package/src/superlocalmemory/cli/version_banner.py +17 -3
- package/src/superlocalmemory/compliance/audit.py +6 -0
- package/src/superlocalmemory/compliance/gdpr.py +128 -138
- package/src/superlocalmemory/compliance/retention.py +176 -45
- package/src/superlocalmemory/core/backend_orchestrator.py +23 -59
- package/src/superlocalmemory/core/community_summary.py +267 -0
- package/src/superlocalmemory/core/config.py +216 -3
- package/src/superlocalmemory/core/consolidation_engine.py +95 -22
- package/src/superlocalmemory/core/context_cache.py +61 -18
- package/src/superlocalmemory/core/embedding_worker.py +21 -7
- package/src/superlocalmemory/core/embeddings.py +131 -46
- package/src/superlocalmemory/core/engine.py +41 -22
- package/src/superlocalmemory/core/engine_ingestion.py +359 -43
- package/src/superlocalmemory/core/engine_wiring.py +13 -0
- package/src/superlocalmemory/core/entity_community.py +178 -0
- package/src/superlocalmemory/core/graph_analyzer.py +39 -2
- package/src/superlocalmemory/core/graph_pruner.py +13 -8
- package/src/superlocalmemory/core/ingestion_command.py +134 -25
- package/src/superlocalmemory/core/injection.py +12 -7
- package/src/superlocalmemory/core/key_expander.py +138 -0
- package/src/superlocalmemory/core/maintenance.py +23 -0
- package/src/superlocalmemory/core/maintenance_scheduler.py +17 -7
- package/src/superlocalmemory/core/modes.py +1 -1
- package/src/superlocalmemory/core/mutations.py +2 -2
- package/src/superlocalmemory/core/pii.py +105 -0
- package/src/superlocalmemory/core/progressive_abstraction.py +208 -0
- package/src/superlocalmemory/core/recall_pipeline.py +7 -3
- package/src/superlocalmemory/core/recall_worker.py +20 -6
- package/src/superlocalmemory/core/scale_engine.py +60 -1
- package/src/superlocalmemory/core/security_primitives.py +40 -2
- package/src/superlocalmemory/core/store_pipeline.py +186 -29
- package/src/superlocalmemory/core/worker_pool.py +21 -6
- package/src/superlocalmemory/encoding/entity_reflexion.py +200 -0
- package/src/superlocalmemory/encoding/entity_resolver.py +34 -24
- package/src/superlocalmemory/encoding/fact_extractor.py +26 -1
- package/src/superlocalmemory/encoding/temporal_validator.py +64 -1
- package/src/superlocalmemory/evolution/evolution_store.py +122 -45
- package/src/superlocalmemory/evolution/llm_dispatch.py +12 -1
- package/src/superlocalmemory/evolution/model_selection.py +160 -0
- package/src/superlocalmemory/evolution/mutation_generator.py +16 -0
- package/src/superlocalmemory/evolution/skill_evolver.py +127 -42
- package/src/superlocalmemory/evolution/triggers.py +22 -13
- package/src/superlocalmemory/graph/cozo_backend.py +43 -20
- package/src/superlocalmemory/hooks/adapter_base.py +5 -1
- package/src/superlocalmemory/hooks/auto_recall.py +13 -1
- package/src/superlocalmemory/hooks/claude_code_hooks.py +11 -0
- package/src/superlocalmemory/hooks/codex_assets.py +64 -5
- package/src/superlocalmemory/hooks/hook_daemon.py +20 -3
- package/src/superlocalmemory/hooks/hook_handlers.py +6 -1
- package/src/superlocalmemory/hooks/memory_protocol.py +54 -0
- package/src/superlocalmemory/hooks/portable_kit.py +148 -3
- package/src/superlocalmemory/infra/backup.py +12 -1
- package/src/superlocalmemory/infra/daemon_identity.py +40 -4
- package/src/superlocalmemory/infra/data_root.py +43 -4
- package/src/superlocalmemory/infra/event_bus.py +107 -24
- package/src/superlocalmemory/infra/rate_limiter.py +93 -0
- package/src/superlocalmemory/ingestion/adapter_manager.py +4 -1
- package/src/superlocalmemory/ingestion/credentials.py +1 -1
- package/src/superlocalmemory/learning/cross_project.py +28 -19
- package/src/superlocalmemory/learning/model_rollback.py +3 -0
- package/src/superlocalmemory/learning/ranker_retrain_online.py +2 -0
- package/src/superlocalmemory/learning/reward.py +50 -0
- package/src/superlocalmemory/learning/reward_proxy.py +42 -9
- package/src/superlocalmemory/learning/source_quality.py +523 -1
- package/src/superlocalmemory/loops/__init__.py +56 -0
- package/src/superlocalmemory/loops/budget.py +58 -0
- package/src/superlocalmemory/loops/engine.py +164 -0
- package/src/superlocalmemory/loops/ledger.py +263 -0
- package/src/superlocalmemory/loops/models.py +152 -0
- package/src/superlocalmemory/loops/rules.py +52 -0
- package/src/superlocalmemory/mcp/_daemon_proxy.py +3 -0
- package/src/superlocalmemory/mcp/_pool_adapter.py +2 -0
- package/src/superlocalmemory/mcp/profiles.py +103 -0
- package/src/superlocalmemory/mcp/server.py +32 -79
- package/src/superlocalmemory/mcp/tools_active.py +4 -7
- package/src/superlocalmemory/mcp/tools_code_graph.py +51 -5
- package/src/superlocalmemory/mcp/tools_core.py +12 -4
- package/src/superlocalmemory/mcp/tools_evolution.py +6 -3
- package/src/superlocalmemory/mcp/tools_learning.py +2 -2
- package/src/superlocalmemory/mcp/tools_loops.py +300 -0
- package/src/superlocalmemory/mcp/tools_mesh.py +140 -4
- package/src/superlocalmemory/mcp/tools_optimize.py +15 -8
- package/src/superlocalmemory/mesh/broker.py +237 -129
- package/src/superlocalmemory/mesh/remote_sync.py +50 -8
- package/src/superlocalmemory/optimize/NOTICE +1 -6
- package/src/superlocalmemory/optimize/adapters/anthropic_adapter.py +1 -4
- package/src/superlocalmemory/optimize/adapters/openai_adapter.py +1 -4
- package/src/superlocalmemory/optimize/cache/semantic.py +27 -19
- package/src/superlocalmemory/optimize/compress/align.py +32 -26
- package/src/superlocalmemory/optimize/compress/ccr.py +14 -71
- package/src/superlocalmemory/optimize/compress/router.py +105 -22
- package/src/superlocalmemory/optimize/config/defaults.py +1 -1
- package/src/superlocalmemory/optimize/config/schema.py +87 -4
- package/src/superlocalmemory/optimize/metrics/counters.py +13 -4
- package/src/superlocalmemory/optimize/metrics/estimator.py +0 -3
- package/src/superlocalmemory/optimize/proxy/_helpers.py +31 -4
- package/src/superlocalmemory/optimize/storage/db.py +38 -9
- package/src/superlocalmemory/optimize/storage/schema.py +10 -0
- package/src/superlocalmemory/parameterization/pattern_extractor.py +6 -3
- package/src/superlocalmemory/retrieval/agentic.py +1 -1
- package/src/superlocalmemory/retrieval/bm25_channel.py +68 -10
- package/src/superlocalmemory/retrieval/engine.py +221 -47
- package/src/superlocalmemory/retrieval/entity_channel.py +7 -5
- package/src/superlocalmemory/retrieval/hopfield_channel.py +9 -2
- package/src/superlocalmemory/retrieval/reranker.py +3 -4
- package/src/superlocalmemory/retrieval/semantic_channel.py +114 -21
- package/src/superlocalmemory/retrieval/spreading_activation.py +11 -2
- package/src/superlocalmemory/retrieval/temporal_channel.py +48 -9
- package/src/superlocalmemory/retrieval/temporal_frame.py +102 -0
- package/src/superlocalmemory/retrieval/temporal_validity_filter.py +135 -0
- package/src/superlocalmemory/retrieval/time_window.py +181 -0
- package/src/superlocalmemory/server/api.py +4 -4
- package/src/superlocalmemory/server/config_file.py +90 -0
- package/src/superlocalmemory/server/origin.py +50 -0
- package/src/superlocalmemory/server/profile_runtime.py +125 -8
- package/src/superlocalmemory/server/rbac_enforce.py +142 -0
- package/src/superlocalmemory/server/recall_health.py +24 -3
- package/src/superlocalmemory/server/recall_serializer.py +19 -1
- package/src/superlocalmemory/server/routes/abstraction.py +115 -0
- package/src/superlocalmemory/server/routes/agents.py +128 -38
- package/src/superlocalmemory/server/routes/backup.py +317 -70
- package/src/superlocalmemory/server/routes/behavioral.py +349 -71
- package/src/superlocalmemory/server/routes/brain.py +69 -12
- package/src/superlocalmemory/server/routes/chat.py +10 -5
- package/src/superlocalmemory/server/routes/compliance.py +171 -21
- package/src/superlocalmemory/server/routes/config_api.py +438 -0
- package/src/superlocalmemory/server/routes/data_io.py +30 -8
- package/src/superlocalmemory/server/routes/entity.py +108 -26
- package/src/superlocalmemory/server/routes/events.py +24 -8
- package/src/superlocalmemory/server/routes/evolution.py +189 -68
- package/src/superlocalmemory/server/routes/helpers.py +16 -1
- package/src/superlocalmemory/server/routes/ingest.py +7 -4
- package/src/superlocalmemory/server/routes/insights.py +3 -3
- package/src/superlocalmemory/server/routes/learning.py +289 -118
- package/src/superlocalmemory/server/routes/learning_telemetry.py +153 -0
- package/src/superlocalmemory/server/routes/lifecycle.py +59 -8
- package/src/superlocalmemory/server/routes/memories.py +182 -57
- package/src/superlocalmemory/server/routes/mesh.py +200 -31
- package/src/superlocalmemory/server/routes/optimize.py +33 -1
- package/src/superlocalmemory/server/routes/prewarm.py +2 -0
- package/src/superlocalmemory/server/routes/profiles.py +63 -17
- package/src/superlocalmemory/server/routes/ratelimit.py +132 -0
- package/src/superlocalmemory/server/routes/rbac.py +367 -0
- package/src/superlocalmemory/server/routes/stats.py +103 -158
- package/src/superlocalmemory/server/routes/tiers.py +11 -9
- package/src/superlocalmemory/server/routes/token.py +3 -13
- package/src/superlocalmemory/server/routes/v3_api.py +247 -89
- package/src/superlocalmemory/server/routes/ws.py +5 -2
- package/src/superlocalmemory/server/security_middleware.py +12 -5
- package/src/superlocalmemory/server/ui.py +20 -5
- package/src/superlocalmemory/server/unified_daemon.py +827 -72
- package/src/superlocalmemory/server/write_identity.py +38 -8
- package/src/superlocalmemory/storage/database.py +265 -53
- package/src/superlocalmemory/storage/migration_runner.py +132 -1
- package/src/superlocalmemory/storage/migrations/M010_evolution_config.py +5 -0
- package/src/superlocalmemory/storage/migrations/M021_ingestion_log_profile.py +108 -0
- package/src/superlocalmemory/storage/migrations/M022_entity_aliases_profile.py +86 -0
- package/src/superlocalmemory/storage/migrations/M023_mesh_profile_isolation.py +194 -0
- package/src/superlocalmemory/storage/migrations/M024_rbac_users_roles.py +87 -0
- package/src/superlocalmemory/storage/migrations/M025_perf_indexes.py +90 -0
- package/src/superlocalmemory/storage/migrations/M026_rbac_memberships_fk.py +136 -0
- package/src/superlocalmemory/storage/migrations/M027_transferable_patterns_profile.py +163 -0
- package/src/superlocalmemory/storage/migrations/M028_fact_entity_associations.py +270 -0
- package/src/superlocalmemory/storage/migrations/M029_behavioral_history_indexes.py +137 -0
- package/src/superlocalmemory/storage/migrations/M030_entity_explorer_indexes.py +93 -0
- package/src/superlocalmemory/storage/migrations/__init__.py +4 -0
- package/src/superlocalmemory/storage/models.py +4 -0
- package/src/superlocalmemory/storage/schema.py +136 -1
- package/src/superlocalmemory/storage/schema_v32.py +2 -0
- package/src/superlocalmemory/storage/schema_v343.py +24 -12
- package/src/superlocalmemory/storage/schema_v347.py +4 -0
- package/src/superlocalmemory/trust/gate.py +49 -8
- package/src/superlocalmemory/ui/assets/slm-icon-white.svg +64 -0
- package/src/superlocalmemory/ui/assets/slm-icon.svg +36 -0
- package/src/superlocalmemory/ui/css/design-system.css +621 -0
- package/src/superlocalmemory/ui/css/neural-glass.css +6 -0
- package/src/superlocalmemory/ui/css/od-bridge.css +158 -0
- package/src/superlocalmemory/ui/favicon.svg +35 -4
- package/src/superlocalmemory/ui/index.html +303 -173
- package/src/superlocalmemory/ui/js/brain.js +5 -20
- package/src/superlocalmemory/ui/js/core.js +100 -41
- package/src/superlocalmemory/ui/js/dashboard.js +403 -65
- package/src/superlocalmemory/ui/js/event-delegation.js +102 -0
- package/src/superlocalmemory/ui/js/knowledge-graph.js +11 -11
- package/src/superlocalmemory/ui/js/math-health.js +1 -1
- package/src/superlocalmemory/ui/js/memories.js +15 -4
- package/src/superlocalmemory/ui/js/memory-chat.js +7 -7
- package/src/superlocalmemory/ui/js/ng-entities.js +6 -8
- package/src/superlocalmemory/ui/js/ng-ingestion.js +4 -4
- package/src/superlocalmemory/ui/js/ng-mesh.js +4 -9
- package/src/superlocalmemory/ui/js/ng-shell.js +8 -8
- package/src/superlocalmemory/ui/js/ng-skills.js +54 -2
- package/src/superlocalmemory/ui/js/od-agents.js +544 -0
- package/src/superlocalmemory/ui/js/od-auth-gate.js +257 -0
- package/src/superlocalmemory/ui/js/od-backup.js +871 -0
- package/src/superlocalmemory/ui/js/od-brain.js +816 -0
- package/src/superlocalmemory/ui/js/od-entities.js +579 -0
- package/src/superlocalmemory/ui/js/od-graph.js +600 -0
- package/src/superlocalmemory/ui/js/od-health.js +539 -0
- package/src/superlocalmemory/ui/js/od-mcp.js +508 -0
- package/src/superlocalmemory/ui/js/od-memories.js +929 -0
- package/src/superlocalmemory/ui/js/od-mesh.js +553 -0
- package/src/superlocalmemory/ui/js/od-operations.js +1250 -0
- package/src/superlocalmemory/ui/js/od-optimize.js +787 -0
- package/src/superlocalmemory/ui/js/od-settings.js +1107 -0
- package/src/superlocalmemory/ui/js/od-shell.js +809 -0
- package/src/superlocalmemory/ui/js/od-skills.js +600 -0
- package/src/superlocalmemory/ui/js/od-team.js +258 -0
- package/src/superlocalmemory/ui/js/profiles.js +159 -46
- package/src/superlocalmemory/ui/js/settings.js +17 -3
- package/src/superlocalmemory/ui/js/timeline.js +34 -5
- package/src/superlocalmemory/ui/js/trust-dashboard.js +2 -2
- package/src/superlocalmemory/vector/lancedb_backend.py +8 -6
- package/plugin-src/.mcp.json +0 -12
- package/plugin-src/agents/slm-memory-advisor.md +0 -44
- package/plugin-src/agents/slm-optimize-advisor.md +0 -38
- package/plugin-src/hooks/.gitkeep +0 -0
- package/plugin-src/hooks/hooks.json +0 -23
- package/plugin-src/manifest.json +0 -25
- package/plugin-src/requirements.txt +0 -1
- package/plugin-src/rules/CLAUDE.md.fragment +0 -44
- package/plugin-src/scripts/ensure-venv.bat +0 -122
- package/plugin-src/scripts/ensure-venv.sh +0 -105
- package/plugin-src/scripts/slm-launch +0 -23
- package/plugin-src/scripts/slm-launch.bat +0 -23
- package/plugin-src/settings.json +0 -16
- package/src/superlocalmemory/learning/behavioral_listener.py +0 -94
|
@@ -5,72 +5,308 @@
|
|
|
5
5
|
- AGPL-3.0-or-later
|
|
6
6
|
|
|
7
7
|
Routes: /api/behavioral/status, /api/behavioral/report-outcome
|
|
8
|
-
Uses V3 learning.behavioral.BehavioralPatternStore and
|
|
8
|
+
Uses V3 learning.behavioral.BehavioralPatternStore and direct telemetry reads.
|
|
9
9
|
"""
|
|
10
10
|
import json
|
|
11
11
|
import logging
|
|
12
|
+
import sqlite3
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Literal
|
|
12
15
|
|
|
13
|
-
from fastapi import APIRouter
|
|
16
|
+
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
|
17
|
+
from pydantic import BaseModel, ConfigDict, Field, StrictStr, field_validator
|
|
14
18
|
|
|
15
|
-
from .helpers import
|
|
19
|
+
from .helpers import MEMORY_DIR, get_active_profile
|
|
16
20
|
|
|
17
21
|
logger = logging.getLogger("superlocalmemory.routes.behavioral")
|
|
18
22
|
router = APIRouter()
|
|
19
23
|
|
|
20
|
-
|
|
24
|
+
_RECENT_OUTCOMES_LIMIT = 20
|
|
25
|
+
_REWARD_TIMELINE_DAYS = 182
|
|
26
|
+
_MAX_OUTCOME_FACT_IDS = 100
|
|
27
|
+
_MAX_FACT_ID_LENGTH = 200
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ReportOutcomeRequest(BaseModel):
|
|
31
|
+
"""Bounded explicit outcome payload accepted from dashboard clients."""
|
|
32
|
+
|
|
33
|
+
model_config = ConfigDict(extra="forbid")
|
|
34
|
+
|
|
35
|
+
memory_ids: list[StrictStr] = Field(
|
|
36
|
+
min_length=1,
|
|
37
|
+
max_length=_MAX_OUTCOME_FACT_IDS,
|
|
38
|
+
)
|
|
39
|
+
outcome: Literal["success", "failure", "partial"]
|
|
40
|
+
action_type: StrictStr = Field(default="other", max_length=80)
|
|
41
|
+
context: StrictStr = Field(default="", max_length=1000)
|
|
42
|
+
|
|
43
|
+
@field_validator("memory_ids")
|
|
44
|
+
@classmethod
|
|
45
|
+
def normalize_fact_ids(cls, value: list[str]) -> list[str]:
|
|
46
|
+
"""Strip and de-duplicate fact IDs while preserving request order."""
|
|
47
|
+
deduplicated: list[str] = []
|
|
48
|
+
seen: set[str] = set()
|
|
49
|
+
for raw_fact_id in value:
|
|
50
|
+
fact_id = raw_fact_id.strip()
|
|
51
|
+
if not fact_id:
|
|
52
|
+
raise ValueError("memory_ids must not contain blank fact IDs")
|
|
53
|
+
if len(fact_id) > _MAX_FACT_ID_LENGTH:
|
|
54
|
+
raise ValueError(
|
|
55
|
+
f"memory_ids entries must be at most "
|
|
56
|
+
f"{_MAX_FACT_ID_LENGTH} characters"
|
|
57
|
+
)
|
|
58
|
+
if fact_id not in seen:
|
|
59
|
+
seen.add(fact_id)
|
|
60
|
+
deduplicated.append(fact_id)
|
|
61
|
+
return deduplicated
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _require_read(request: Request) -> None:
|
|
65
|
+
from superlocalmemory.access.rbac import Permission
|
|
66
|
+
from superlocalmemory.server.rbac_enforce import require_permission
|
|
67
|
+
|
|
68
|
+
require_permission(request, Permission.READ, profile=get_active_profile())
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _require_write(request: Request) -> None:
|
|
72
|
+
from superlocalmemory.access.rbac import Permission
|
|
73
|
+
from superlocalmemory.server.rbac_enforce import require_permission
|
|
74
|
+
|
|
75
|
+
require_permission(request, Permission.WRITE, profile=get_active_profile())
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _authorize_outcome_write(request: Request) -> None:
|
|
79
|
+
"""Run authorization before FastAPI validates the request body."""
|
|
80
|
+
_require_write(request)
|
|
81
|
+
request.state.outcome_write_authorized = True
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _validate_profile_fact_ids(
|
|
85
|
+
conn: sqlite3.Connection,
|
|
86
|
+
*,
|
|
87
|
+
profile_id: str,
|
|
88
|
+
fact_ids: list[str],
|
|
89
|
+
) -> None:
|
|
90
|
+
"""Reject missing or foreign-profile facts before an outcome is stored."""
|
|
91
|
+
placeholders = ",".join("?" for _ in fact_ids)
|
|
92
|
+
rows = conn.execute(
|
|
93
|
+
"SELECT fact_id FROM atomic_facts "
|
|
94
|
+
f"WHERE profile_id = ? AND fact_id IN ({placeholders})",
|
|
95
|
+
(profile_id, *fact_ids),
|
|
96
|
+
).fetchall()
|
|
97
|
+
if {str(row[0]) for row in rows} != set(fact_ids):
|
|
98
|
+
raise HTTPException(
|
|
99
|
+
status_code=422,
|
|
100
|
+
detail="Every memory_id must identify a fact in the active profile",
|
|
101
|
+
)
|
|
102
|
+
|
|
21
103
|
|
|
22
104
|
# Feature detection
|
|
23
105
|
BEHAVIORAL_AVAILABLE = False
|
|
24
106
|
try:
|
|
25
107
|
from superlocalmemory.learning.behavioral import BehavioralPatternStore
|
|
26
|
-
from superlocalmemory.learning.outcomes import OutcomeTracker
|
|
27
108
|
BEHAVIORAL_AVAILABLE = True
|
|
28
109
|
except ImportError as e:
|
|
29
110
|
logger.warning("V3 behavioral engine import failed: %s", e)
|
|
30
111
|
|
|
31
112
|
|
|
113
|
+
def _memory_db_path() -> Path:
|
|
114
|
+
"""Resolve at read time so tests and profile-scoped routes stay aligned."""
|
|
115
|
+
return MEMORY_DIR / "memory.db"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _learning_db_path() -> Path:
|
|
119
|
+
return MEMORY_DIR / "learning.db"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _is_cross_project_pattern(pattern: dict) -> bool:
|
|
123
|
+
metadata = pattern.get("metadata")
|
|
124
|
+
return (
|
|
125
|
+
isinstance(metadata, dict)
|
|
126
|
+
and bool(str(metadata.get("transferred_from") or "").strip())
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _load_action_outcomes(profile_id: str) -> dict:
|
|
131
|
+
"""Read bounded explicit/finalized outcome telemetry from ``memory.db``.
|
|
132
|
+
|
|
133
|
+
``OutcomeTracker`` requires a ``DatabaseManager`` and the outcome table
|
|
134
|
+
belongs to ``memory.db``. This read-only query deliberately does not
|
|
135
|
+
infer outcomes from recall hits, which are exposure signals rather than
|
|
136
|
+
evidence that the returned memory helped.
|
|
137
|
+
"""
|
|
138
|
+
empty = {
|
|
139
|
+
"total": 0,
|
|
140
|
+
"breakdown": {"success": 0, "failure": 0, "partial": 0},
|
|
141
|
+
"recent": [],
|
|
142
|
+
"reward": _empty_reward_telemetry(),
|
|
143
|
+
}
|
|
144
|
+
db_path = _memory_db_path()
|
|
145
|
+
if not db_path.exists():
|
|
146
|
+
return empty
|
|
147
|
+
try:
|
|
148
|
+
conn = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True, timeout=1.0)
|
|
149
|
+
conn.row_factory = sqlite3.Row
|
|
150
|
+
try:
|
|
151
|
+
columns = {
|
|
152
|
+
str(row["name"])
|
|
153
|
+
for row in conn.execute(
|
|
154
|
+
"PRAGMA table_info(action_outcomes)",
|
|
155
|
+
).fetchall()
|
|
156
|
+
}
|
|
157
|
+
rows = conn.execute(
|
|
158
|
+
"SELECT outcome, COUNT(*) AS count FROM action_outcomes "
|
|
159
|
+
"WHERE profile_id = ? "
|
|
160
|
+
"AND outcome IN ('success', 'failure', 'partial') "
|
|
161
|
+
"GROUP BY outcome",
|
|
162
|
+
(profile_id,),
|
|
163
|
+
).fetchall()
|
|
164
|
+
recent = conn.execute(
|
|
165
|
+
"SELECT outcome, context_json, timestamp FROM action_outcomes "
|
|
166
|
+
"WHERE profile_id = ? "
|
|
167
|
+
"AND outcome IN ('success', 'failure', 'partial') "
|
|
168
|
+
"ORDER BY timestamp DESC LIMIT ?",
|
|
169
|
+
(profile_id, _RECENT_OUTCOMES_LIMIT),
|
|
170
|
+
).fetchall()
|
|
171
|
+
reward = (
|
|
172
|
+
_query_reward_telemetry(conn, profile_id, columns)
|
|
173
|
+
if {"reward", "settled"}.issubset(columns)
|
|
174
|
+
else _empty_reward_telemetry()
|
|
175
|
+
)
|
|
176
|
+
finally:
|
|
177
|
+
conn.close()
|
|
178
|
+
except sqlite3.Error as exc:
|
|
179
|
+
logger.debug("action_outcomes telemetry unavailable: %s", exc)
|
|
180
|
+
return empty
|
|
181
|
+
breakdown = {"success": 0, "failure": 0, "partial": 0}
|
|
182
|
+
for row in rows:
|
|
183
|
+
if row["outcome"] in breakdown:
|
|
184
|
+
breakdown[row["outcome"]] = int(row["count"] or 0)
|
|
185
|
+
return {
|
|
186
|
+
"total": sum(breakdown.values()),
|
|
187
|
+
"breakdown": breakdown,
|
|
188
|
+
"recent": [_outcome_preview(row) for row in recent],
|
|
189
|
+
"reward": reward,
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _empty_reward_telemetry() -> dict:
|
|
194
|
+
return {
|
|
195
|
+
"count": 0,
|
|
196
|
+
"average": None,
|
|
197
|
+
"distribution": {"positive": 0, "neutral": 0, "negative": 0},
|
|
198
|
+
"timeline": [],
|
|
199
|
+
"source": "memory.db:action_outcomes.reward",
|
|
200
|
+
"window_days": _REWARD_TIMELINE_DAYS,
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _query_reward_telemetry(
|
|
205
|
+
conn: sqlite3.Connection,
|
|
206
|
+
profile_id: str,
|
|
207
|
+
columns: set[str],
|
|
208
|
+
) -> dict:
|
|
209
|
+
"""Aggregate numeric settled labels without materializing reward rows."""
|
|
210
|
+
settled_time = (
|
|
211
|
+
"COALESCE(settled_at, timestamp)"
|
|
212
|
+
if "settled_at" in columns
|
|
213
|
+
else "timestamp"
|
|
214
|
+
)
|
|
215
|
+
aggregate = conn.execute(
|
|
216
|
+
"SELECT COUNT(*) AS count, AVG(reward) AS average, "
|
|
217
|
+
"SUM(CASE WHEN reward > 0.6 THEN 1 ELSE 0 END) AS positive, "
|
|
218
|
+
"SUM(CASE WHEN reward < 0.4 THEN 1 ELSE 0 END) AS negative, "
|
|
219
|
+
"SUM(CASE WHEN reward >= 0.4 AND reward <= 0.6 "
|
|
220
|
+
"THEN 1 ELSE 0 END) AS neutral "
|
|
221
|
+
"FROM action_outcomes WHERE profile_id = ? AND settled = 1 "
|
|
222
|
+
"AND reward IS NOT NULL AND typeof(reward) IN ('integer', 'real')",
|
|
223
|
+
(profile_id,),
|
|
224
|
+
).fetchone()
|
|
225
|
+
timeline = conn.execute(
|
|
226
|
+
"WITH reward_days AS ("
|
|
227
|
+
f" SELECT substr({settled_time}, 1, 10) AS day,"
|
|
228
|
+
" reward"
|
|
229
|
+
" FROM action_outcomes"
|
|
230
|
+
" WHERE profile_id = ? AND settled = 1 AND reward IS NOT NULL"
|
|
231
|
+
" AND typeof(reward) IN ('integer', 'real')"
|
|
232
|
+
f"), latest AS (SELECT MAX(day) AS day FROM reward_days)"
|
|
233
|
+
" SELECT reward_days.day AS date, COUNT(*) AS count,"
|
|
234
|
+
" AVG(reward_days.reward) AS average"
|
|
235
|
+
" FROM reward_days, latest"
|
|
236
|
+
" WHERE reward_days.day >= date(latest.day, ?)"
|
|
237
|
+
" GROUP BY reward_days.day ORDER BY reward_days.day ASC LIMIT ?",
|
|
238
|
+
(
|
|
239
|
+
profile_id,
|
|
240
|
+
f"-{_REWARD_TIMELINE_DAYS - 1} days",
|
|
241
|
+
_REWARD_TIMELINE_DAYS,
|
|
242
|
+
),
|
|
243
|
+
).fetchall()
|
|
244
|
+
count = int(aggregate["count"] or 0)
|
|
245
|
+
return {
|
|
246
|
+
"count": count,
|
|
247
|
+
"average": (
|
|
248
|
+
round(float(aggregate["average"]), 4) if count else None
|
|
249
|
+
),
|
|
250
|
+
"distribution": {
|
|
251
|
+
"positive": int(aggregate["positive"] or 0),
|
|
252
|
+
"neutral": int(aggregate["neutral"] or 0),
|
|
253
|
+
"negative": int(aggregate["negative"] or 0),
|
|
254
|
+
},
|
|
255
|
+
"timeline": [
|
|
256
|
+
{
|
|
257
|
+
"date": str(row["date"]),
|
|
258
|
+
"count": int(row["count"] or 0),
|
|
259
|
+
"average": round(float(row["average"] or 0.0), 4),
|
|
260
|
+
}
|
|
261
|
+
for row in timeline
|
|
262
|
+
if row["date"]
|
|
263
|
+
],
|
|
264
|
+
"source": "memory.db:action_outcomes.reward",
|
|
265
|
+
"window_days": _REWARD_TIMELINE_DAYS,
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _outcome_preview(row: sqlite3.Row) -> dict:
|
|
270
|
+
"""Return a safe, structured summary without exposing free-form notes."""
|
|
271
|
+
try:
|
|
272
|
+
context = json.loads(str(row["context_json"] or "{}"))
|
|
273
|
+
except (TypeError, ValueError, json.JSONDecodeError):
|
|
274
|
+
context = {}
|
|
275
|
+
action_type = context.get("action_type", "other")
|
|
276
|
+
return {
|
|
277
|
+
"outcome": str(row["outcome"] or "partial"),
|
|
278
|
+
"action_type": str(action_type)[:80],
|
|
279
|
+
"timestamp": str(row["timestamp"] or ""),
|
|
280
|
+
"source": "memory.db:action_outcomes",
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
|
|
32
284
|
@router.get("/api/behavioral/status")
|
|
33
|
-
|
|
285
|
+
def behavioral_status():
|
|
34
286
|
"""Get behavioral learning status for active profile."""
|
|
35
287
|
if not BEHAVIORAL_AVAILABLE:
|
|
36
288
|
return {"available": False, "message": "Behavioral engine not available"}
|
|
37
289
|
|
|
38
290
|
try:
|
|
39
291
|
profile = get_active_profile()
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
recent_outcomes = []
|
|
46
|
-
try:
|
|
47
|
-
tracker = OutcomeTracker(db_path)
|
|
48
|
-
all_outcomes = tracker.get_outcomes(profile_id=profile, limit=50)
|
|
49
|
-
total_outcomes = len(all_outcomes)
|
|
50
|
-
for o in all_outcomes:
|
|
51
|
-
key = o.outcome if hasattr(o, 'outcome') else str(o)
|
|
52
|
-
if key in outcome_breakdown:
|
|
53
|
-
outcome_breakdown[key] += 1
|
|
54
|
-
recent_outcomes = [
|
|
55
|
-
{"outcome": o.outcome, "action_type": o.action_type,
|
|
56
|
-
"timestamp": o.timestamp}
|
|
57
|
-
for o in all_outcomes[:20]
|
|
58
|
-
if hasattr(o, 'outcome')
|
|
59
|
-
]
|
|
60
|
-
except Exception as exc:
|
|
61
|
-
logger.debug("outcome tracker: %s", exc)
|
|
292
|
+
outcome_data = _load_action_outcomes(profile)
|
|
293
|
+
total_outcomes = outcome_data["total"]
|
|
294
|
+
outcome_breakdown = outcome_data["breakdown"]
|
|
295
|
+
recent_outcomes = outcome_data["recent"]
|
|
296
|
+
reward_telemetry = outcome_data["reward"]
|
|
62
297
|
|
|
63
298
|
# Patterns
|
|
64
299
|
patterns = []
|
|
300
|
+
cross_project_patterns = []
|
|
65
301
|
cross_project_transfers = 0
|
|
66
302
|
try:
|
|
67
|
-
store = BehavioralPatternStore(
|
|
303
|
+
store = BehavioralPatternStore(str(_learning_db_path()))
|
|
68
304
|
patterns = store.get_patterns(profile_id=profile)
|
|
69
|
-
|
|
70
|
-
cross_project_transfers = len([
|
|
305
|
+
cross_project_patterns = [
|
|
71
306
|
p for p in patterns
|
|
72
|
-
if isinstance(p, dict) and p
|
|
73
|
-
]
|
|
307
|
+
if isinstance(p, dict) and _is_cross_project_pattern(p)
|
|
308
|
+
]
|
|
309
|
+
cross_project_transfers = len(cross_project_patterns)
|
|
74
310
|
except Exception as exc:
|
|
75
311
|
logger.warning("pattern store error: %s", exc)
|
|
76
312
|
|
|
@@ -79,9 +315,14 @@ async def behavioral_status():
|
|
|
79
315
|
"active_profile": profile,
|
|
80
316
|
"total_outcomes": total_outcomes,
|
|
81
317
|
"outcome_breakdown": outcome_breakdown,
|
|
318
|
+
"outcomes_source": "memory.db:action_outcomes",
|
|
319
|
+
"outcomes_are_finalized": True,
|
|
320
|
+
"outcomes_provenance": "explicit_reports_or_finalized_signals",
|
|
82
321
|
"patterns": patterns,
|
|
83
322
|
"cross_project_transfers": cross_project_transfers,
|
|
323
|
+
"cross_project_patterns": cross_project_patterns,
|
|
84
324
|
"recent_outcomes": recent_outcomes,
|
|
325
|
+
"reward_telemetry": reward_telemetry,
|
|
85
326
|
"stats": {
|
|
86
327
|
"success_count": outcome_breakdown.get("success", 0),
|
|
87
328
|
"failure_count": outcome_breakdown.get("failure", 0),
|
|
@@ -89,13 +330,16 @@ async def behavioral_status():
|
|
|
89
330
|
"patterns_count": len(patterns),
|
|
90
331
|
},
|
|
91
332
|
}
|
|
92
|
-
except Exception
|
|
93
|
-
logger.
|
|
94
|
-
return {"available": False, "error":
|
|
333
|
+
except Exception:
|
|
334
|
+
logger.exception("behavioral_status error")
|
|
335
|
+
return {"available": False, "error": "Internal server error"}
|
|
95
336
|
|
|
96
337
|
|
|
97
|
-
@router.post(
|
|
98
|
-
|
|
338
|
+
@router.post(
|
|
339
|
+
"/api/behavioral/report-outcome",
|
|
340
|
+
dependencies=[Depends(_authorize_outcome_write)],
|
|
341
|
+
)
|
|
342
|
+
def report_outcome(request: Request, data: ReportOutcomeRequest):
|
|
99
343
|
"""Record an explicit dashboard-reported outcome.
|
|
100
344
|
|
|
101
345
|
Body: {
|
|
@@ -113,20 +357,18 @@ async def report_outcome(data: dict):
|
|
|
113
357
|
from ``outcome``:
|
|
114
358
|
success=1.0, failure=0.0, partial=0.5
|
|
115
359
|
"""
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
return {"success": False, "error": f"outcome must be one of: {valid_outcomes}"}
|
|
360
|
+
if not getattr(request.state, "outcome_write_authorized", False):
|
|
361
|
+
_require_write(request)
|
|
362
|
+
if isinstance(data, dict):
|
|
363
|
+
# Preserve the long-standing direct-call API while applying the same
|
|
364
|
+
# constrained model used by FastAPI at the HTTP boundary.
|
|
365
|
+
data = ReportOutcomeRequest.model_validate(data)
|
|
366
|
+
memory_ids = data.memory_ids
|
|
367
|
+
outcome = data.outcome
|
|
368
|
+
action_type = data.action_type
|
|
369
|
+
context_note = data.context
|
|
127
370
|
|
|
128
371
|
import sqlite3
|
|
129
|
-
import time
|
|
130
372
|
import uuid
|
|
131
373
|
from datetime import datetime, timezone
|
|
132
374
|
|
|
@@ -146,6 +388,12 @@ async def report_outcome(data: dict):
|
|
|
146
388
|
conn = sqlite3.connect(str(memory_db_path), timeout=5.0)
|
|
147
389
|
try:
|
|
148
390
|
conn.execute("PRAGMA busy_timeout=5000")
|
|
391
|
+
conn.execute("BEGIN IMMEDIATE")
|
|
392
|
+
_validate_profile_fact_ids(
|
|
393
|
+
conn,
|
|
394
|
+
profile_id=profile,
|
|
395
|
+
fact_ids=memory_ids,
|
|
396
|
+
)
|
|
149
397
|
conn.execute(
|
|
150
398
|
"INSERT INTO action_outcomes "
|
|
151
399
|
"(outcome_id, profile_id, query, fact_ids_json, outcome, "
|
|
@@ -163,6 +411,21 @@ async def report_outcome(data: dict):
|
|
|
163
411
|
finally:
|
|
164
412
|
conn.close()
|
|
165
413
|
|
|
414
|
+
try:
|
|
415
|
+
from superlocalmemory.learning.source_quality import (
|
|
416
|
+
update_source_quality_for_reward,
|
|
417
|
+
)
|
|
418
|
+
update_source_quality_for_reward(
|
|
419
|
+
memory_db_path=memory_db_path,
|
|
420
|
+
learning_db_path=_learning_db_path(),
|
|
421
|
+
profile_id=profile,
|
|
422
|
+
outcome_id=outcome_id,
|
|
423
|
+
fact_ids=[str(memory_id) for memory_id in memory_ids],
|
|
424
|
+
reward=reward,
|
|
425
|
+
)
|
|
426
|
+
except Exception as exc: # noqa: BLE001 - outcome write already committed
|
|
427
|
+
logger.debug("source-quality explicit outcome feed skipped: %s", exc)
|
|
428
|
+
|
|
166
429
|
return {
|
|
167
430
|
"success": True, "outcome_id": outcome_id,
|
|
168
431
|
"active_profile": profile,
|
|
@@ -172,9 +435,11 @@ async def report_outcome(data: dict):
|
|
|
172
435
|
f"memories (reward={reward})"
|
|
173
436
|
),
|
|
174
437
|
}
|
|
175
|
-
except
|
|
176
|
-
|
|
177
|
-
|
|
438
|
+
except HTTPException:
|
|
439
|
+
raise
|
|
440
|
+
except Exception:
|
|
441
|
+
logger.exception("report_outcome error")
|
|
442
|
+
return {"success": False, "error": "Internal server error"}
|
|
178
443
|
|
|
179
444
|
|
|
180
445
|
# --------------------------------------------------------------------------
|
|
@@ -182,7 +447,11 @@ async def report_outcome(data: dict):
|
|
|
182
447
|
# --------------------------------------------------------------------------
|
|
183
448
|
|
|
184
449
|
@router.get("/api/behavioral/assertions")
|
|
185
|
-
|
|
450
|
+
def get_assertions(
|
|
451
|
+
min_confidence: float = Query(default=0.0, ge=0.0, le=1.0),
|
|
452
|
+
category: str = Query(default="", max_length=100),
|
|
453
|
+
limit: int = Query(default=50, ge=1, le=1000),
|
|
454
|
+
):
|
|
186
455
|
"""Get learned behavioral assertions for dashboard display."""
|
|
187
456
|
try:
|
|
188
457
|
import sqlite3 as _sqlite3
|
|
@@ -213,18 +482,20 @@ async def get_assertions(min_confidence: float = 0.0, category: str = "", limit:
|
|
|
213
482
|
"count": len(assertions),
|
|
214
483
|
"active_profile": profile,
|
|
215
484
|
}
|
|
216
|
-
except Exception
|
|
217
|
-
logger.
|
|
218
|
-
return {"assertions": [], "count": 0, "error":
|
|
485
|
+
except Exception:
|
|
486
|
+
logger.exception("get_assertions error")
|
|
487
|
+
return {"assertions": [], "count": 0, "error": "Internal server error"}
|
|
219
488
|
|
|
220
489
|
|
|
221
490
|
@router.get("/api/behavioral/tool-events")
|
|
222
|
-
|
|
491
|
+
def get_tool_events(
|
|
492
|
+
tool_name: str = "",
|
|
493
|
+
limit: int = Query(default=100, ge=1, le=1000),
|
|
494
|
+
):
|
|
223
495
|
"""Get recent tool events for dashboard display."""
|
|
224
496
|
try:
|
|
225
497
|
import sqlite3 as _sqlite3
|
|
226
498
|
profile = get_active_profile()
|
|
227
|
-
limit = min(int(limit), 1000)
|
|
228
499
|
conn = _sqlite3.connect(str(MEMORY_DIR / "memory.db"))
|
|
229
500
|
conn.row_factory = _sqlite3.Row
|
|
230
501
|
|
|
@@ -246,35 +517,40 @@ async def get_tool_events(tool_name: str = "", limit: int = 100):
|
|
|
246
517
|
return {"events": events, "count": len(events)}
|
|
247
518
|
finally:
|
|
248
519
|
conn.close()
|
|
249
|
-
except Exception
|
|
250
|
-
logger.
|
|
251
|
-
return {"events": [], "count": 0, "error":
|
|
520
|
+
except Exception:
|
|
521
|
+
logger.exception("get_tool_events error")
|
|
522
|
+
return {"events": [], "count": 0, "error": "Internal server error"}
|
|
252
523
|
|
|
253
524
|
|
|
254
525
|
@router.get("/api/behavioral/soft-prompts")
|
|
255
|
-
|
|
526
|
+
def get_soft_prompts(request: Request):
|
|
256
527
|
"""Get active soft prompt templates for dashboard display."""
|
|
528
|
+
_require_read(request)
|
|
257
529
|
try:
|
|
258
530
|
import sqlite3 as _sqlite3
|
|
531
|
+
profile = get_active_profile()
|
|
259
532
|
conn = _sqlite3.connect(str(MEMORY_DIR / "memory.db"))
|
|
260
533
|
conn.row_factory = _sqlite3.Row
|
|
261
534
|
rows = conn.execute(
|
|
262
535
|
"SELECT prompt_id, category, content, confidence, effectiveness, "
|
|
263
536
|
"token_count, active, version, created_at "
|
|
264
|
-
"FROM soft_prompt_templates
|
|
537
|
+
"FROM soft_prompt_templates "
|
|
538
|
+
"WHERE profile_id = ? AND active = 1 "
|
|
539
|
+
"ORDER BY category, prompt_id",
|
|
540
|
+
(profile,),
|
|
265
541
|
).fetchall()
|
|
266
542
|
conn.close()
|
|
267
543
|
return {"prompts": [dict(zip(
|
|
268
544
|
["prompt_id", "category", "content", "confidence", "effectiveness",
|
|
269
545
|
"token_count", "active", "version", "created_at"], r
|
|
270
546
|
)) for r in rows], "count": len(rows)}
|
|
271
|
-
except Exception
|
|
272
|
-
logger.
|
|
273
|
-
return {"prompts": [], "count": 0, "error":
|
|
547
|
+
except Exception:
|
|
548
|
+
logger.exception("get_soft_prompts error")
|
|
549
|
+
return {"prompts": [], "count": 0, "error": "Internal server error"}
|
|
274
550
|
|
|
275
551
|
|
|
276
552
|
@router.post("/api/v3/tool-event")
|
|
277
|
-
|
|
553
|
+
def log_tool_event_api(request: Request, data: dict):
|
|
278
554
|
"""Log a tool event via HTTP (called by PostToolUse hook).
|
|
279
555
|
|
|
280
556
|
Body (v3.4.10 enriched):
|
|
@@ -290,10 +566,11 @@ async def log_tool_event_api(data: dict):
|
|
|
290
566
|
All fields except tool_name are optional for backward compatibility.
|
|
291
567
|
Lightweight — no LLM, just an INSERT.
|
|
292
568
|
"""
|
|
569
|
+
_require_write(request)
|
|
293
570
|
try:
|
|
571
|
+
import os
|
|
294
572
|
import sqlite3 as _sqlite3
|
|
295
573
|
from datetime import datetime, timezone
|
|
296
|
-
import os
|
|
297
574
|
|
|
298
575
|
tool_name = data.get("tool_name", "unknown")
|
|
299
576
|
event_type = data.get("event_type", "complete")
|
|
@@ -322,5 +599,6 @@ async def log_tool_event_api(data: dict):
|
|
|
322
599
|
finally:
|
|
323
600
|
conn.close()
|
|
324
601
|
return {"ok": True}
|
|
325
|
-
except Exception
|
|
326
|
-
|
|
602
|
+
except Exception:
|
|
603
|
+
logger.exception("behavioral route error")
|
|
604
|
+
return {"ok": False, "error": "Internal server error"}
|