superlocalmemory 3.4.19 → 3.4.21
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 +24 -0
- package/README.md +42 -34
- package/bin/slm +11 -0
- package/bin/slm.bat +12 -0
- package/package.json +4 -3
- package/pyproject.toml +3 -2
- package/scripts/build-slm-hook.ps1 +40 -0
- package/scripts/build-slm-hook.sh +45 -0
- package/scripts/build_entry.py +452 -0
- package/scripts/ci/stage5b_gate.sh +50 -0
- package/scripts/postinstall/validation.js +187 -0
- package/scripts/postinstall-interactive.js +756 -0
- package/scripts/postinstall_binary.js +287 -0
- package/scripts/release_manifest.py +273 -0
- package/scripts/slm-hook.spec +56 -0
- package/skills/slm-build-graph/SKILL.md +423 -0
- package/skills/slm-list-recent/SKILL.md +348 -0
- package/skills/slm-recall/SKILL.md +343 -0
- package/skills/slm-remember/SKILL.md +194 -0
- package/skills/slm-show-patterns/SKILL.md +224 -0
- package/skills/slm-status/SKILL.md +363 -0
- package/skills/slm-switch-profile/SKILL.md +442 -0
- package/src/superlocalmemory/cli/commands.py +219 -79
- package/src/superlocalmemory/cli/context_commands.py +192 -0
- package/src/superlocalmemory/cli/daemon.py +15 -1
- package/src/superlocalmemory/cli/db_migrate.py +80 -0
- package/src/superlocalmemory/cli/escape_hatch.py +220 -0
- package/src/superlocalmemory/cli/main.py +72 -1
- package/src/superlocalmemory/core/context_cache.py +397 -0
- package/src/superlocalmemory/core/engine.py +38 -2
- package/src/superlocalmemory/core/engine_wiring.py +1 -1
- package/src/superlocalmemory/core/ram_lock.py +111 -0
- package/src/superlocalmemory/core/recall_pipeline.py +433 -3
- package/src/superlocalmemory/core/recall_worker.py +8 -3
- package/src/superlocalmemory/core/security_primitives.py +635 -0
- package/src/superlocalmemory/core/shadow_router.py +319 -0
- package/src/superlocalmemory/core/slm_disabled.py +87 -0
- package/src/superlocalmemory/core/slmignore.py +125 -0
- package/src/superlocalmemory/core/topic_signature.py +143 -0
- package/src/superlocalmemory/core/worker_pool.py +14 -3
- package/src/superlocalmemory/encoding/cognitive_consolidator.py +2 -2
- package/src/superlocalmemory/evolution/budget.py +321 -0
- package/src/superlocalmemory/evolution/llm_dispatch.py +508 -0
- package/src/superlocalmemory/evolution/skill_evolver.py +144 -94
- package/src/superlocalmemory/hooks/_outcome_common.py +506 -0
- package/src/superlocalmemory/hooks/adapter_base.py +317 -0
- package/src/superlocalmemory/hooks/antigravity_adapter.py +192 -0
- package/src/superlocalmemory/hooks/claude_code_hooks.py +33 -1
- package/src/superlocalmemory/hooks/context_payload.py +312 -0
- package/src/superlocalmemory/hooks/copilot_adapter.py +154 -0
- package/src/superlocalmemory/hooks/cross_platform_connector.py +90 -0
- package/src/superlocalmemory/hooks/cursor_adapter.py +195 -0
- package/src/superlocalmemory/hooks/hook_handlers.py +109 -8
- package/src/superlocalmemory/hooks/ide_connector.py +25 -2
- package/src/superlocalmemory/hooks/post_tool_async_hook.py +165 -0
- package/src/superlocalmemory/hooks/post_tool_outcome_hook.py +223 -0
- package/src/superlocalmemory/hooks/prewarm_auth.py +170 -0
- package/src/superlocalmemory/hooks/session_registry.py +186 -0
- package/src/superlocalmemory/hooks/stop_outcome_hook.py +134 -0
- package/src/superlocalmemory/hooks/sync_loop.py +114 -0
- package/src/superlocalmemory/hooks/user_prompt_hook.py +128 -0
- package/src/superlocalmemory/hooks/user_prompt_rehash_hook.py +202 -0
- package/src/superlocalmemory/infra/backup.py +3 -3
- package/src/superlocalmemory/infra/cloud_backup.py +2 -2
- package/src/superlocalmemory/infra/event_bus.py +2 -2
- package/src/superlocalmemory/infra/webhook_dispatcher.py +3 -3
- package/src/superlocalmemory/learning/arm_catalog.py +99 -0
- package/src/superlocalmemory/learning/bandit.py +526 -0
- package/src/superlocalmemory/learning/bandit_cache.py +133 -0
- package/src/superlocalmemory/learning/behavioral.py +53 -1
- package/src/superlocalmemory/learning/consolidation_cycle.py +381 -0
- package/src/superlocalmemory/learning/consolidation_worker.py +188 -520
- package/src/superlocalmemory/learning/database.py +256 -0
- package/src/superlocalmemory/learning/dedup_hnsw.py +413 -0
- package/src/superlocalmemory/learning/ensemble.py +300 -0
- package/src/superlocalmemory/learning/fact_outcome_joins.py +207 -0
- package/src/superlocalmemory/learning/forgetting_scheduler.py +55 -0
- package/src/superlocalmemory/learning/hnsw_dedup.py +69 -0
- package/src/superlocalmemory/learning/labeler.py +87 -0
- package/src/superlocalmemory/learning/legacy_migration.py +277 -0
- package/src/superlocalmemory/learning/memory_merge.py +160 -0
- package/src/superlocalmemory/learning/model_cache.py +269 -0
- package/src/superlocalmemory/learning/model_rollback.py +278 -0
- package/src/superlocalmemory/learning/outcome_queue.py +284 -0
- package/src/superlocalmemory/learning/pattern_miner.py +415 -0
- package/src/superlocalmemory/learning/pattern_miner_constants.py +47 -0
- package/src/superlocalmemory/learning/ranker.py +225 -81
- package/src/superlocalmemory/learning/ranker_common.py +163 -0
- package/src/superlocalmemory/learning/ranker_retrain_legacy.py +202 -0
- package/src/superlocalmemory/learning/ranker_retrain_online.py +411 -0
- package/src/superlocalmemory/learning/reward.py +777 -0
- package/src/superlocalmemory/learning/reward_archive.py +210 -0
- package/src/superlocalmemory/learning/reward_boost.py +201 -0
- package/src/superlocalmemory/learning/reward_proxy.py +326 -0
- package/src/superlocalmemory/learning/shadow_test.py +524 -0
- package/src/superlocalmemory/learning/signal_worker.py +270 -0
- package/src/superlocalmemory/learning/signals.py +314 -0
- package/src/superlocalmemory/learning/trigram_index.py +547 -0
- package/src/superlocalmemory/mcp/server.py +5 -5
- package/src/superlocalmemory/mcp/tools_context.py +183 -0
- package/src/superlocalmemory/mcp/tools_core.py +92 -27
- package/src/superlocalmemory/parameterization/soft_prompt_generator.py +13 -0
- package/src/superlocalmemory/retrieval/engine.py +52 -0
- package/src/superlocalmemory/server/api.py +2 -2
- package/src/superlocalmemory/server/bandit_loops.py +140 -0
- package/src/superlocalmemory/server/middleware/__init__.py +11 -0
- package/src/superlocalmemory/server/middleware/security_headers.py +144 -0
- package/src/superlocalmemory/server/routes/backup.py +36 -13
- package/src/superlocalmemory/server/routes/behavioral.py +50 -19
- package/src/superlocalmemory/server/routes/brain.py +1234 -0
- package/src/superlocalmemory/server/routes/data_io.py +4 -4
- package/src/superlocalmemory/server/routes/events.py +2 -2
- package/src/superlocalmemory/server/routes/helpers.py +1 -1
- package/src/superlocalmemory/server/routes/learning.py +192 -7
- package/src/superlocalmemory/server/routes/memories.py +189 -1
- package/src/superlocalmemory/server/routes/prewarm.py +171 -0
- package/src/superlocalmemory/server/routes/profiles.py +3 -3
- package/src/superlocalmemory/server/routes/token.py +88 -0
- package/src/superlocalmemory/server/routes/ws.py +5 -5
- package/src/superlocalmemory/server/security_middleware.py +13 -7
- package/src/superlocalmemory/server/ui.py +2 -2
- package/src/superlocalmemory/server/unified_daemon.py +335 -3
- package/src/superlocalmemory/storage/migration_runner.py +545 -0
- package/src/superlocalmemory/storage/migrations/M001_add_signal_features_columns.py +67 -0
- package/src/superlocalmemory/storage/migrations/M002_model_state_history.py +132 -0
- package/src/superlocalmemory/storage/migrations/M003_migration_log.py +38 -0
- package/src/superlocalmemory/storage/migrations/M004_cross_platform_sync_log.py +46 -0
- package/src/superlocalmemory/storage/migrations/M005_bandit_tables.py +75 -0
- package/src/superlocalmemory/storage/migrations/M006_action_outcomes_reward.py +75 -0
- package/src/superlocalmemory/storage/migrations/M007_pending_outcomes.py +63 -0
- package/src/superlocalmemory/storage/migrations/M009_model_lineage.py +54 -0
- package/src/superlocalmemory/storage/migrations/M010_evolution_config.py +75 -0
- package/src/superlocalmemory/storage/migrations/M011_archive_and_merge.py +87 -0
- package/src/superlocalmemory/storage/migrations/M012_shadow_observations.py +72 -0
- package/src/superlocalmemory/storage/migrations/M013_bi_temporal_columns.py +55 -0
- package/src/superlocalmemory/storage/migrations/__init__.py +81 -0
- package/src/superlocalmemory/storage/models.py +4 -0
- package/src/superlocalmemory/ui/css/brain.css +409 -0
- package/src/superlocalmemory/ui/css/legacy-dashboard.css +645 -0
- package/src/superlocalmemory/ui/index.html +459 -1345
- package/src/superlocalmemory/ui/js/brain.js +1321 -0
- package/src/superlocalmemory/ui/js/clusters.js +123 -4
- package/src/superlocalmemory/ui/js/init.js +48 -39
- package/src/superlocalmemory/ui/js/memories.js +88 -2
- package/src/superlocalmemory/ui/js/modal.js +71 -1
- package/src/superlocalmemory/ui/js/ng-shell.js +101 -88
- package/src/superlocalmemory/ui/js/trust-dashboard.js +168 -25
- package/src/superlocalmemory/ui/vendor/bootstrap-icons/bootstrap-icons.css +2018 -0
- package/src/superlocalmemory/ui/vendor/bootstrap-icons/fonts/bootstrap-icons.woff +0 -0
- package/src/superlocalmemory/ui/vendor/bootstrap-icons/fonts/bootstrap-icons.woff2 +0 -0
- package/src/superlocalmemory/ui/vendor/bootstrap.bundle.min.js +7 -0
- package/src/superlocalmemory/ui/vendor/bootstrap.min.css +6 -0
- package/src/superlocalmemory/ui/vendor/d3.v7.min.js +2 -0
- package/src/superlocalmemory/ui/vendor/graphology-library.min.js +2 -0
- package/src/superlocalmemory/ui/vendor/graphology.umd.min.js +2 -0
- package/src/superlocalmemory/ui/vendor/inter-ui/inter-variable.min.css +8 -0
- package/src/superlocalmemory/ui/vendor/inter-ui/variable/InterVariable-Italic.woff2 +0 -0
- package/src/superlocalmemory/ui/vendor/inter-ui/variable/InterVariable.woff2 +0 -0
- package/src/superlocalmemory/ui/vendor/sigma.min.js +1 -0
- package/src/superlocalmemory/ui/js/behavioral.js +0 -447
- package/src/superlocalmemory/ui/js/graph-core.js +0 -447
- package/src/superlocalmemory/ui/js/graph-interactions.js +0 -351
- package/src/superlocalmemory/ui/js/learning.js +0 -435
- package/src/superlocalmemory/ui/js/patterns.js +0 -93
- package/src/superlocalmemory.egg-info/PKG-INFO +0 -647
- package/src/superlocalmemory.egg-info/SOURCES.txt +0 -335
- 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 -58
- package/src/superlocalmemory.egg-info/top_level.txt +0 -1
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
|
|
2
|
+
# Licensed under AGPL-3.0-or-later - see LICENSE file
|
|
3
|
+
# Part of SuperLocalMemory v3.4.21 — LLD-04 §4.2 (v2)
|
|
4
|
+
|
|
5
|
+
"""Strict security-headers ASGI middleware for the Brain UI.
|
|
6
|
+
|
|
7
|
+
LLD-04 §4.2 v2 — deterministic, allow-list-only policy:
|
|
8
|
+
|
|
9
|
+
content-security-policy: default-src 'self'; script-src 'self';
|
|
10
|
+
style-src 'self'; img-src 'self' data:;
|
|
11
|
+
connect-src 'self'; frame-ancestors 'none';
|
|
12
|
+
base-uri 'self'; form-action 'self'
|
|
13
|
+
x-content-type-options: nosniff
|
|
14
|
+
x-frame-options: DENY
|
|
15
|
+
referrer-policy: no-referrer
|
|
16
|
+
permissions-policy: interest-cohort=(), microphone=(),
|
|
17
|
+
camera=(), geolocation=()
|
|
18
|
+
|
|
19
|
+
Design decisions (from LLD-04 §4.2 Policy block):
|
|
20
|
+
|
|
21
|
+
* **No CSP nonce.** Nonce plumbing was the most common v1 bug; removing
|
|
22
|
+
it entirely means there is literally no path for an inline script/style
|
|
23
|
+
to execute — ``script-src 'self'`` blocks all of them.
|
|
24
|
+
* **Deterministic bytes.** The header tuple is a module-level constant.
|
|
25
|
+
No per-request computation, no string concatenation, no dependency on
|
|
26
|
+
request state. This keeps the middleware O(1) and cheap.
|
|
27
|
+
* **Works on error responses.** Because we wrap ``send`` itself, the
|
|
28
|
+
headers land on 401s, 500s, and body-less responses alike — not only
|
|
29
|
+
on successful handlers. Regression tested in ``test_headers.py``.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
from starlette.types import ASGIApp, Receive, Scope, Send
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# Deterministic, module-level, immutable. Every request gets the same bytes.
|
|
38
|
+
# Keeping the declaration as a tuple of (name, value) bytes pairs avoids any
|
|
39
|
+
# per-request allocation in the hot path.
|
|
40
|
+
_HEADERS: tuple[tuple[bytes, bytes], ...] = (
|
|
41
|
+
(
|
|
42
|
+
b"content-security-policy",
|
|
43
|
+
(
|
|
44
|
+
b"default-src 'self'; "
|
|
45
|
+
b"script-src 'self'; "
|
|
46
|
+
b"style-src 'self'; "
|
|
47
|
+
b"img-src 'self' data:; "
|
|
48
|
+
b"connect-src 'self'; "
|
|
49
|
+
b"frame-ancestors 'none'; "
|
|
50
|
+
b"base-uri 'self'; "
|
|
51
|
+
b"form-action 'self'"
|
|
52
|
+
),
|
|
53
|
+
),
|
|
54
|
+
(b"x-content-type-options", b"nosniff"),
|
|
55
|
+
(b"x-frame-options", b"DENY"),
|
|
56
|
+
(b"referrer-policy", b"no-referrer"),
|
|
57
|
+
(
|
|
58
|
+
b"permissions-policy",
|
|
59
|
+
b"interest-cohort=(), microphone=(), camera=(), geolocation=()",
|
|
60
|
+
),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# Set of header names (lower-case bytes) we own. We strip existing values
|
|
65
|
+
# from downstream responses so we never double-up if another middleware
|
|
66
|
+
# already set a looser variant.
|
|
67
|
+
_OWNED_NAMES: frozenset[bytes] = frozenset(name for name, _ in _HEADERS)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# LLD-04 §4.2 v2 scope note: the strict policy applies to the Brain API and
|
|
71
|
+
# any Brain-scoped static endpoints. Pre-existing dashboard routes (``/``,
|
|
72
|
+
# ``/ui/*``, ``/static/*``, other tabs) currently load vendor libraries
|
|
73
|
+
# (Bootstrap, Bootstrap Icons, Inter font, D3, Sigma, graphology) from CDNs
|
|
74
|
+
# and would break under ``script-src 'self'`` until those assets are vendored
|
|
75
|
+
# locally (tracked for v3.4.21 vendoring work). Applying the strict set there
|
|
76
|
+
# produced a user-visible regression during Stage 6 delivery-lead review,
|
|
77
|
+
# which contradicts LLD-04's user-benefit goal. This middleware therefore
|
|
78
|
+
# enforces the strict set on the routes that actually need it today —
|
|
79
|
+
# ``/api/v3/brain*`` and the deprecated brain shims — and is a no-op on
|
|
80
|
+
# everything else. The existing ``server/security_middleware.py`` keeps
|
|
81
|
+
# emitting its legacy headers on the remaining routes.
|
|
82
|
+
_STRICT_SCOPE_PREFIXES: tuple[bytes, ...] = (
|
|
83
|
+
b"/api/v3/brain",
|
|
84
|
+
b"/api/v3/learning/stats",
|
|
85
|
+
b"/api/v3/patterns",
|
|
86
|
+
b"/api/v3/behavioral",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _is_strict_path(raw_path: bytes) -> bool:
|
|
91
|
+
for prefix in _STRICT_SCOPE_PREFIXES:
|
|
92
|
+
if raw_path == prefix or raw_path.startswith(prefix + b"/") \
|
|
93
|
+
or raw_path.startswith(prefix + b"?"):
|
|
94
|
+
return True
|
|
95
|
+
return False
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class SecurityHeadersMiddleware:
|
|
99
|
+
"""ASGI middleware that injects the strict v2 header set on Brain routes.
|
|
100
|
+
|
|
101
|
+
Unlike ``BaseHTTPMiddleware``, this wraps ``send`` directly so we can
|
|
102
|
+
act on the ``http.response.start`` event before the client sees any
|
|
103
|
+
headers. That guarantees coverage of exception responses, ``Response``
|
|
104
|
+
objects from ``HTTPException`` handlers, and streaming responses — all
|
|
105
|
+
of which skip ``BaseHTTPMiddleware``'s dispatch flow in edge cases.
|
|
106
|
+
|
|
107
|
+
Scope: see ``_STRICT_SCOPE_PREFIXES``. Outside that scope the middleware
|
|
108
|
+
passes through untouched so pre-existing dashboard CSP policy still
|
|
109
|
+
governs index/static responses until the vendoring work in v3.4.21.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
def __init__(self, app: ASGIApp) -> None:
|
|
113
|
+
self.app = app
|
|
114
|
+
|
|
115
|
+
async def __call__(
|
|
116
|
+
self, scope: Scope, receive: Receive, send: Send,
|
|
117
|
+
) -> None:
|
|
118
|
+
# Only HTTP responses get headers. Websocket / lifespan skip.
|
|
119
|
+
if scope.get("type") != "http":
|
|
120
|
+
return await self.app(scope, receive, send)
|
|
121
|
+
|
|
122
|
+
raw_path = scope.get("raw_path") or scope.get("path", "").encode("latin-1")
|
|
123
|
+
if not _is_strict_path(raw_path):
|
|
124
|
+
return await self.app(scope, receive, send)
|
|
125
|
+
|
|
126
|
+
async def _send(message: dict) -> None:
|
|
127
|
+
if message.get("type") == "http.response.start":
|
|
128
|
+
existing = list(message.get("headers") or [])
|
|
129
|
+
# Strip any existing copies of our owned headers so we
|
|
130
|
+
# never emit duplicates.
|
|
131
|
+
filtered = [
|
|
132
|
+
(name, value)
|
|
133
|
+
for name, value in existing
|
|
134
|
+
if name.lower() not in _OWNED_NAMES
|
|
135
|
+
]
|
|
136
|
+
for name, value in _HEADERS:
|
|
137
|
+
filtered.append((name, value))
|
|
138
|
+
message["headers"] = filtered
|
|
139
|
+
await send(message)
|
|
140
|
+
|
|
141
|
+
await self.app(scope, receive, _send)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
__all__ = ("SecurityHeadersMiddleware", "_is_strict_path", "_STRICT_SCOPE_PREFIXES")
|
|
@@ -303,13 +303,36 @@ p {{ color: #999; margin: 0 0 20px; font-size: 13px; }}
|
|
|
303
303
|
</div></body></html>"""
|
|
304
304
|
|
|
305
305
|
|
|
306
|
+
# S8-SEC-04: OAuth success / error pages render user-influenced strings
|
|
307
|
+
# (``error``, ``error_description``, provider ``message`` / ``title``).
|
|
308
|
+
# ``str.format`` doesn't escape HTML, so a hostile OAuth callback can
|
|
309
|
+
# inject ``<script>`` into these pages. These helpers HTML-escape every
|
|
310
|
+
# interpolated value before emitting the template.
|
|
311
|
+
import html as _html
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _oauth_error_page(icon: str, error: str) -> str:
|
|
315
|
+
return _OAUTH_ERROR_HTML.format(
|
|
316
|
+
icon=_html.escape(str(icon), quote=True),
|
|
317
|
+
error=_html.escape(str(error), quote=True),
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _oauth_success_page(icon: str, title: str, message: str) -> str:
|
|
322
|
+
return _OAUTH_SUCCESS_HTML.format(
|
|
323
|
+
icon=_html.escape(str(icon), quote=True),
|
|
324
|
+
title=_html.escape(str(title), quote=True),
|
|
325
|
+
message=_html.escape(str(message), quote=True),
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
|
|
306
329
|
# ---- Google OAuth SSO Flow ------------------------------------------------
|
|
307
330
|
|
|
308
331
|
@router.get("/api/backup/oauth/google/start")
|
|
309
332
|
async def google_oauth_start(request: Request):
|
|
310
333
|
"""Start Google OAuth2 flow — redirects to Google's login page."""
|
|
311
334
|
if not CLOUD_AVAILABLE:
|
|
312
|
-
return HTMLResponse(
|
|
335
|
+
return HTMLResponse(_oauth_error_page(icon="⚠", error="Cloud backup module not available"))
|
|
313
336
|
|
|
314
337
|
from superlocalmemory.infra.cloud_backup import _get_credential
|
|
315
338
|
|
|
@@ -424,10 +447,10 @@ async function saveAndConnect() {
|
|
|
424
447
|
async def google_oauth_callback(request: Request, code: str = "", error: str = ""):
|
|
425
448
|
"""Google OAuth2 callback — exchanges code for tokens."""
|
|
426
449
|
if error:
|
|
427
|
-
return HTMLResponse(
|
|
450
|
+
return HTMLResponse(_oauth_error_page(icon="❌", error=f"Google denied access: {error}"))
|
|
428
451
|
|
|
429
452
|
if not code:
|
|
430
|
-
return HTMLResponse(
|
|
453
|
+
return HTMLResponse(_oauth_error_page(icon="❌", error="No authorization code received"))
|
|
431
454
|
|
|
432
455
|
base_url = str(request.base_url).rstrip("/")
|
|
433
456
|
redirect_uri = f"{base_url}/api/backup/oauth/google/callback"
|
|
@@ -435,9 +458,9 @@ async def google_oauth_callback(request: Request, code: str = "", error: str = "
|
|
|
435
458
|
result = connect_google_drive(code, redirect_uri)
|
|
436
459
|
|
|
437
460
|
if "error" in result:
|
|
438
|
-
return HTMLResponse(
|
|
461
|
+
return HTMLResponse(_oauth_error_page(icon="❌", error=result["error"]))
|
|
439
462
|
|
|
440
|
-
return HTMLResponse(
|
|
463
|
+
return HTMLResponse(_oauth_success_page(
|
|
441
464
|
icon="☁️",
|
|
442
465
|
title="Google Drive Connected!",
|
|
443
466
|
message=f"Signed in as {result.get('email', 'unknown')}. Your memories will be backed up automatically."
|
|
@@ -453,7 +476,7 @@ async def google_oauth_callback(request: Request, code: str = "", error: str = "
|
|
|
453
476
|
async def github_oauth_start(request: Request):
|
|
454
477
|
"""Start GitHub OAuth flow."""
|
|
455
478
|
if not CLOUD_AVAILABLE:
|
|
456
|
-
return HTMLResponse(
|
|
479
|
+
return HTMLResponse(_oauth_error_page(icon="⚠", error="Cloud backup module not available"))
|
|
457
480
|
|
|
458
481
|
from superlocalmemory.infra.cloud_backup import _get_credential
|
|
459
482
|
|
|
@@ -553,10 +576,10 @@ async function doConnect() {
|
|
|
553
576
|
async def github_oauth_callback(request: Request, code: str = "", error: str = ""):
|
|
554
577
|
"""GitHub OAuth callback — exchanges code for access token."""
|
|
555
578
|
if error:
|
|
556
|
-
return HTMLResponse(
|
|
579
|
+
return HTMLResponse(_oauth_error_page(icon="❌", error=f"GitHub denied access: {error}"))
|
|
557
580
|
|
|
558
581
|
if not code:
|
|
559
|
-
return HTMLResponse(
|
|
582
|
+
return HTMLResponse(_oauth_error_page(icon="❌", error="No authorization code received"))
|
|
560
583
|
|
|
561
584
|
from superlocalmemory.infra.cloud_backup import _get_credential, _store_credential
|
|
562
585
|
import httpx
|
|
@@ -565,7 +588,7 @@ async def github_oauth_callback(request: Request, code: str = "", error: str = "
|
|
|
565
588
|
gh_client_secret = _get_credential("github_client_secret")
|
|
566
589
|
|
|
567
590
|
if not gh_client_id or not gh_client_secret:
|
|
568
|
-
return HTMLResponse(
|
|
591
|
+
return HTMLResponse(_oauth_error_page(icon="❌", error="GitHub OAuth App not configured"))
|
|
569
592
|
|
|
570
593
|
try:
|
|
571
594
|
# Exchange code for access token
|
|
@@ -578,18 +601,18 @@ async def github_oauth_callback(request: Request, code: str = "", error: str = "
|
|
|
578
601
|
data = resp.json()
|
|
579
602
|
access_token = data.get("access_token")
|
|
580
603
|
if not access_token:
|
|
581
|
-
return HTMLResponse(
|
|
604
|
+
return HTMLResponse(_oauth_error_page(icon="❌", error=data.get("error_description", "Failed to get access token")))
|
|
582
605
|
|
|
583
606
|
# Use the token to connect
|
|
584
607
|
result = connect_github(access_token, "slm-backup")
|
|
585
608
|
if "error" in result:
|
|
586
|
-
return HTMLResponse(
|
|
609
|
+
return HTMLResponse(_oauth_error_page(icon="❌", error=result["error"]))
|
|
587
610
|
|
|
588
|
-
return HTMLResponse(
|
|
611
|
+
return HTMLResponse(_oauth_success_page(
|
|
589
612
|
icon="✅",
|
|
590
613
|
title="GitHub Connected!",
|
|
591
614
|
message=f"Repository: {result.get('repo', 'slm-backup')}. Your memories will be backed up automatically."
|
|
592
615
|
))
|
|
593
616
|
|
|
594
617
|
except Exception as exc:
|
|
595
|
-
return HTMLResponse(
|
|
618
|
+
return HTMLResponse(_oauth_error_page(icon="❌", error=str(exc)))
|
|
@@ -96,7 +96,7 @@ async def behavioral_status():
|
|
|
96
96
|
|
|
97
97
|
@router.post("/api/behavioral/report-outcome")
|
|
98
98
|
async def report_outcome(data: dict):
|
|
99
|
-
"""Record an
|
|
99
|
+
"""Record an explicit dashboard-reported outcome.
|
|
100
100
|
|
|
101
101
|
Body: {
|
|
102
102
|
memory_ids: [str, ...],
|
|
@@ -104,10 +104,15 @@ async def report_outcome(data: dict):
|
|
|
104
104
|
action_type: str (optional),
|
|
105
105
|
context: str (optional)
|
|
106
106
|
}
|
|
107
|
-
"""
|
|
108
|
-
if not BEHAVIORAL_AVAILABLE:
|
|
109
|
-
return {"success": False, "error": "Behavioral engine not available"}
|
|
110
107
|
|
|
108
|
+
S9-DASH-02: previously this handler passed a path string to
|
|
109
|
+
``OutcomeTracker(db)`` (which expects a DatabaseManager) and also
|
|
110
|
+
targeted ``learning.db`` — but ``action_outcomes`` lives in
|
|
111
|
+
``memory.db`` (M006). Both failures were silent. This rewrite
|
|
112
|
+
writes directly to ``action_outcomes`` with a reward label derived
|
|
113
|
+
from ``outcome``:
|
|
114
|
+
success=1.0, failure=0.0, partial=0.5
|
|
115
|
+
"""
|
|
111
116
|
memory_ids = data.get('memory_ids')
|
|
112
117
|
outcome = data.get('outcome')
|
|
113
118
|
action_type = data.get('action_type', 'other')
|
|
@@ -120,26 +125,52 @@ async def report_outcome(data: dict):
|
|
|
120
125
|
if outcome not in valid_outcomes:
|
|
121
126
|
return {"success": False, "error": f"outcome must be one of: {valid_outcomes}"}
|
|
122
127
|
|
|
128
|
+
import sqlite3
|
|
129
|
+
import time
|
|
130
|
+
import uuid
|
|
131
|
+
from datetime import datetime, timezone
|
|
132
|
+
|
|
133
|
+
reward_map = {"success": 1.0, "failure": 0.0, "partial": 0.5}
|
|
134
|
+
reward = reward_map[outcome]
|
|
135
|
+
now_iso = datetime.now(timezone.utc).isoformat()
|
|
136
|
+
outcome_id = str(uuid.uuid4())
|
|
137
|
+
memory_db_path = MEMORY_DIR / "memory.db"
|
|
138
|
+
|
|
123
139
|
try:
|
|
124
140
|
profile = get_active_profile()
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
141
|
+
context_dict = {
|
|
142
|
+
"note": context_note,
|
|
143
|
+
"action_type": action_type,
|
|
144
|
+
"source": "dashboard_report_outcome",
|
|
145
|
+
}
|
|
146
|
+
conn = sqlite3.connect(str(memory_db_path), timeout=5.0)
|
|
147
|
+
try:
|
|
148
|
+
conn.execute("PRAGMA busy_timeout=5000")
|
|
149
|
+
conn.execute(
|
|
150
|
+
"INSERT INTO action_outcomes "
|
|
151
|
+
"(outcome_id, profile_id, query, fact_ids_json, outcome, "
|
|
152
|
+
" context_json, timestamp, reward, settled, settled_at) "
|
|
153
|
+
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, 1, ?)",
|
|
154
|
+
(
|
|
155
|
+
outcome_id, profile, "",
|
|
156
|
+
json.dumps(memory_ids),
|
|
157
|
+
outcome,
|
|
158
|
+
json.dumps(context_dict),
|
|
159
|
+
now_iso, reward, now_iso,
|
|
160
|
+
),
|
|
161
|
+
)
|
|
162
|
+
conn.commit()
|
|
163
|
+
finally:
|
|
164
|
+
conn.close()
|
|
138
165
|
|
|
139
166
|
return {
|
|
140
|
-
"success": True, "outcome_id":
|
|
167
|
+
"success": True, "outcome_id": outcome_id,
|
|
141
168
|
"active_profile": profile,
|
|
142
|
-
"
|
|
169
|
+
"reward": reward,
|
|
170
|
+
"message": (
|
|
171
|
+
f"Recorded {outcome} outcome for {len(memory_ids)} "
|
|
172
|
+
f"memories (reward={reward})"
|
|
173
|
+
),
|
|
143
174
|
}
|
|
144
175
|
except Exception as e:
|
|
145
176
|
logger.error("report_outcome error: %s", e)
|