superlocalmemory 3.4.19 → 3.4.22

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.
Files changed (177) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +42 -34
  3. package/bin/slm +11 -0
  4. package/bin/slm.bat +12 -0
  5. package/package.json +4 -3
  6. package/pyproject.toml +4 -3
  7. package/scripts/build-slm-hook.ps1 +40 -0
  8. package/scripts/build-slm-hook.sh +45 -0
  9. package/scripts/build_entry.py +452 -0
  10. package/scripts/ci/stage5b_gate.sh +50 -0
  11. package/scripts/postinstall/validation.js +187 -0
  12. package/scripts/postinstall-interactive.js +756 -0
  13. package/scripts/postinstall_binary.js +287 -0
  14. package/scripts/release_manifest.py +273 -0
  15. package/scripts/slm-hook.spec +56 -0
  16. package/skills/slm-build-graph/SKILL.md +423 -0
  17. package/skills/slm-list-recent/SKILL.md +348 -0
  18. package/skills/slm-recall/SKILL.md +343 -0
  19. package/skills/slm-remember/SKILL.md +194 -0
  20. package/skills/slm-show-patterns/SKILL.md +224 -0
  21. package/skills/slm-status/SKILL.md +363 -0
  22. package/skills/slm-switch-profile/SKILL.md +442 -0
  23. package/src/superlocalmemory/cli/commands.py +254 -79
  24. package/src/superlocalmemory/cli/context_commands.py +192 -0
  25. package/src/superlocalmemory/cli/daemon.py +15 -1
  26. package/src/superlocalmemory/cli/db_migrate.py +80 -0
  27. package/src/superlocalmemory/cli/escape_hatch.py +220 -0
  28. package/src/superlocalmemory/cli/main.py +72 -1
  29. package/src/superlocalmemory/core/context_cache.py +397 -0
  30. package/src/superlocalmemory/core/engine.py +38 -2
  31. package/src/superlocalmemory/core/engine_wiring.py +1 -1
  32. package/src/superlocalmemory/core/ram_lock.py +111 -0
  33. package/src/superlocalmemory/core/recall_pipeline.py +433 -3
  34. package/src/superlocalmemory/core/recall_worker.py +8 -3
  35. package/src/superlocalmemory/core/security_primitives.py +635 -0
  36. package/src/superlocalmemory/core/shadow_router.py +319 -0
  37. package/src/superlocalmemory/core/slm_disabled.py +87 -0
  38. package/src/superlocalmemory/core/slmignore.py +125 -0
  39. package/src/superlocalmemory/core/topic_signature.py +143 -0
  40. package/src/superlocalmemory/core/worker_pool.py +14 -3
  41. package/src/superlocalmemory/encoding/cognitive_consolidator.py +2 -2
  42. package/src/superlocalmemory/evolution/budget.py +321 -0
  43. package/src/superlocalmemory/evolution/llm_dispatch.py +508 -0
  44. package/src/superlocalmemory/evolution/skill_evolver.py +144 -94
  45. package/src/superlocalmemory/hooks/_outcome_common.py +506 -0
  46. package/src/superlocalmemory/hooks/adapter_base.py +317 -0
  47. package/src/superlocalmemory/hooks/antigravity_adapter.py +192 -0
  48. package/src/superlocalmemory/hooks/claude_code_hooks.py +33 -1
  49. package/src/superlocalmemory/hooks/context_payload.py +312 -0
  50. package/src/superlocalmemory/hooks/copilot_adapter.py +154 -0
  51. package/src/superlocalmemory/hooks/cross_platform_connector.py +90 -0
  52. package/src/superlocalmemory/hooks/cursor_adapter.py +195 -0
  53. package/src/superlocalmemory/hooks/hook_handlers.py +109 -8
  54. package/src/superlocalmemory/hooks/ide_connector.py +25 -2
  55. package/src/superlocalmemory/hooks/post_tool_async_hook.py +165 -0
  56. package/src/superlocalmemory/hooks/post_tool_outcome_hook.py +223 -0
  57. package/src/superlocalmemory/hooks/prewarm_auth.py +170 -0
  58. package/src/superlocalmemory/hooks/session_registry.py +186 -0
  59. package/src/superlocalmemory/hooks/stop_outcome_hook.py +134 -0
  60. package/src/superlocalmemory/hooks/sync_loop.py +114 -0
  61. package/src/superlocalmemory/hooks/user_prompt_hook.py +128 -0
  62. package/src/superlocalmemory/hooks/user_prompt_rehash_hook.py +202 -0
  63. package/src/superlocalmemory/infra/backup.py +3 -3
  64. package/src/superlocalmemory/infra/cloud_backup.py +2 -2
  65. package/src/superlocalmemory/infra/event_bus.py +2 -2
  66. package/src/superlocalmemory/infra/webhook_dispatcher.py +3 -3
  67. package/src/superlocalmemory/learning/arm_catalog.py +99 -0
  68. package/src/superlocalmemory/learning/bandit.py +526 -0
  69. package/src/superlocalmemory/learning/bandit_cache.py +133 -0
  70. package/src/superlocalmemory/learning/behavioral.py +53 -1
  71. package/src/superlocalmemory/learning/consolidation_cycle.py +381 -0
  72. package/src/superlocalmemory/learning/consolidation_worker.py +188 -520
  73. package/src/superlocalmemory/learning/database.py +256 -0
  74. package/src/superlocalmemory/learning/dedup_hnsw.py +413 -0
  75. package/src/superlocalmemory/learning/ensemble.py +300 -0
  76. package/src/superlocalmemory/learning/fact_outcome_joins.py +207 -0
  77. package/src/superlocalmemory/learning/forgetting_scheduler.py +55 -0
  78. package/src/superlocalmemory/learning/hnsw_dedup.py +69 -0
  79. package/src/superlocalmemory/learning/labeler.py +87 -0
  80. package/src/superlocalmemory/learning/legacy_migration.py +277 -0
  81. package/src/superlocalmemory/learning/memory_merge.py +160 -0
  82. package/src/superlocalmemory/learning/model_cache.py +269 -0
  83. package/src/superlocalmemory/learning/model_rollback.py +278 -0
  84. package/src/superlocalmemory/learning/outcome_queue.py +284 -0
  85. package/src/superlocalmemory/learning/pattern_miner.py +415 -0
  86. package/src/superlocalmemory/learning/pattern_miner_constants.py +47 -0
  87. package/src/superlocalmemory/learning/ranker.py +225 -81
  88. package/src/superlocalmemory/learning/ranker_common.py +163 -0
  89. package/src/superlocalmemory/learning/ranker_retrain_legacy.py +202 -0
  90. package/src/superlocalmemory/learning/ranker_retrain_online.py +411 -0
  91. package/src/superlocalmemory/learning/reward.py +777 -0
  92. package/src/superlocalmemory/learning/reward_archive.py +210 -0
  93. package/src/superlocalmemory/learning/reward_boost.py +201 -0
  94. package/src/superlocalmemory/learning/reward_proxy.py +326 -0
  95. package/src/superlocalmemory/learning/shadow_test.py +524 -0
  96. package/src/superlocalmemory/learning/signal_worker.py +270 -0
  97. package/src/superlocalmemory/learning/signals.py +314 -0
  98. package/src/superlocalmemory/learning/trigram_index.py +547 -0
  99. package/src/superlocalmemory/mcp/server.py +5 -5
  100. package/src/superlocalmemory/mcp/tools_context.py +183 -0
  101. package/src/superlocalmemory/mcp/tools_core.py +92 -27
  102. package/src/superlocalmemory/parameterization/soft_prompt_generator.py +13 -0
  103. package/src/superlocalmemory/retrieval/engine.py +52 -0
  104. package/src/superlocalmemory/server/api.py +2 -2
  105. package/src/superlocalmemory/server/bandit_loops.py +140 -0
  106. package/src/superlocalmemory/server/middleware/__init__.py +11 -0
  107. package/src/superlocalmemory/server/middleware/security_headers.py +144 -0
  108. package/src/superlocalmemory/server/routes/backup.py +36 -13
  109. package/src/superlocalmemory/server/routes/behavioral.py +50 -19
  110. package/src/superlocalmemory/server/routes/brain.py +1234 -0
  111. package/src/superlocalmemory/server/routes/data_io.py +4 -4
  112. package/src/superlocalmemory/server/routes/events.py +2 -2
  113. package/src/superlocalmemory/server/routes/helpers.py +1 -1
  114. package/src/superlocalmemory/server/routes/learning.py +192 -7
  115. package/src/superlocalmemory/server/routes/memories.py +189 -1
  116. package/src/superlocalmemory/server/routes/prewarm.py +171 -0
  117. package/src/superlocalmemory/server/routes/profiles.py +3 -3
  118. package/src/superlocalmemory/server/routes/token.py +88 -0
  119. package/src/superlocalmemory/server/routes/ws.py +5 -5
  120. package/src/superlocalmemory/server/security_middleware.py +13 -7
  121. package/src/superlocalmemory/server/ui.py +2 -2
  122. package/src/superlocalmemory/server/unified_daemon.py +335 -3
  123. package/src/superlocalmemory/skills/slm-build-graph/SKILL.md +423 -0
  124. package/src/superlocalmemory/skills/slm-list-recent/SKILL.md +348 -0
  125. package/src/superlocalmemory/skills/slm-recall/SKILL.md +343 -0
  126. package/src/superlocalmemory/skills/slm-remember/SKILL.md +194 -0
  127. package/src/superlocalmemory/skills/slm-show-patterns/SKILL.md +224 -0
  128. package/src/superlocalmemory/skills/slm-status/SKILL.md +363 -0
  129. package/src/superlocalmemory/skills/slm-switch-profile/SKILL.md +442 -0
  130. package/src/superlocalmemory/storage/migration_runner.py +545 -0
  131. package/src/superlocalmemory/storage/migrations/M001_add_signal_features_columns.py +67 -0
  132. package/src/superlocalmemory/storage/migrations/M002_model_state_history.py +132 -0
  133. package/src/superlocalmemory/storage/migrations/M003_migration_log.py +38 -0
  134. package/src/superlocalmemory/storage/migrations/M004_cross_platform_sync_log.py +46 -0
  135. package/src/superlocalmemory/storage/migrations/M005_bandit_tables.py +75 -0
  136. package/src/superlocalmemory/storage/migrations/M006_action_outcomes_reward.py +75 -0
  137. package/src/superlocalmemory/storage/migrations/M007_pending_outcomes.py +63 -0
  138. package/src/superlocalmemory/storage/migrations/M009_model_lineage.py +54 -0
  139. package/src/superlocalmemory/storage/migrations/M010_evolution_config.py +75 -0
  140. package/src/superlocalmemory/storage/migrations/M011_archive_and_merge.py +87 -0
  141. package/src/superlocalmemory/storage/migrations/M012_shadow_observations.py +72 -0
  142. package/src/superlocalmemory/storage/migrations/M013_bi_temporal_columns.py +55 -0
  143. package/src/superlocalmemory/storage/migrations/__init__.py +81 -0
  144. package/src/superlocalmemory/storage/models.py +4 -0
  145. package/src/superlocalmemory/ui/css/brain.css +409 -0
  146. package/src/superlocalmemory/ui/css/legacy-dashboard.css +645 -0
  147. package/src/superlocalmemory/ui/index.html +459 -1345
  148. package/src/superlocalmemory/ui/js/brain.js +1321 -0
  149. package/src/superlocalmemory/ui/js/clusters.js +123 -4
  150. package/src/superlocalmemory/ui/js/init.js +48 -39
  151. package/src/superlocalmemory/ui/js/memories.js +88 -2
  152. package/src/superlocalmemory/ui/js/modal.js +71 -1
  153. package/src/superlocalmemory/ui/js/ng-shell.js +101 -88
  154. package/src/superlocalmemory/ui/js/trust-dashboard.js +168 -25
  155. package/src/superlocalmemory/ui/vendor/bootstrap-icons/bootstrap-icons.css +2018 -0
  156. package/src/superlocalmemory/ui/vendor/bootstrap-icons/fonts/bootstrap-icons.woff +0 -0
  157. package/src/superlocalmemory/ui/vendor/bootstrap-icons/fonts/bootstrap-icons.woff2 +0 -0
  158. package/src/superlocalmemory/ui/vendor/bootstrap.bundle.min.js +7 -0
  159. package/src/superlocalmemory/ui/vendor/bootstrap.min.css +6 -0
  160. package/src/superlocalmemory/ui/vendor/d3.v7.min.js +2 -0
  161. package/src/superlocalmemory/ui/vendor/graphology-library.min.js +2 -0
  162. package/src/superlocalmemory/ui/vendor/graphology.umd.min.js +2 -0
  163. package/src/superlocalmemory/ui/vendor/inter-ui/inter-variable.min.css +8 -0
  164. package/src/superlocalmemory/ui/vendor/inter-ui/variable/InterVariable-Italic.woff2 +0 -0
  165. package/src/superlocalmemory/ui/vendor/inter-ui/variable/InterVariable.woff2 +0 -0
  166. package/src/superlocalmemory/ui/vendor/sigma.min.js +1 -0
  167. package/src/superlocalmemory/ui/js/behavioral.js +0 -447
  168. package/src/superlocalmemory/ui/js/graph-core.js +0 -447
  169. package/src/superlocalmemory/ui/js/graph-interactions.js +0 -351
  170. package/src/superlocalmemory/ui/js/learning.js +0 -435
  171. package/src/superlocalmemory/ui/js/patterns.js +0 -93
  172. package/src/superlocalmemory.egg-info/PKG-INFO +0 -647
  173. package/src/superlocalmemory.egg-info/SOURCES.txt +0 -335
  174. package/src/superlocalmemory.egg-info/dependency_links.txt +0 -1
  175. package/src/superlocalmemory.egg-info/entry_points.txt +0 -2
  176. package/src/superlocalmemory.egg-info/requires.txt +0 -58
  177. 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.22 — 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.22 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.22.
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(_OAUTH_ERROR_HTML.format(icon="&#x26A0;", error="Cloud backup module not available"))
335
+ return HTMLResponse(_oauth_error_page(icon="&#x26A0;", 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(_OAUTH_ERROR_HTML.format(icon="&#x274C;", error=f"Google denied access: {error}"))
450
+ return HTMLResponse(_oauth_error_page(icon="&#x274C;", error=f"Google denied access: {error}"))
428
451
 
429
452
  if not code:
430
- return HTMLResponse(_OAUTH_ERROR_HTML.format(icon="&#x274C;", error="No authorization code received"))
453
+ return HTMLResponse(_oauth_error_page(icon="&#x274C;", 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(_OAUTH_ERROR_HTML.format(icon="&#x274C;", error=result["error"]))
461
+ return HTMLResponse(_oauth_error_page(icon="&#x274C;", error=result["error"]))
439
462
 
440
- return HTMLResponse(_OAUTH_SUCCESS_HTML.format(
463
+ return HTMLResponse(_oauth_success_page(
441
464
  icon="&#x2601;&#xFE0F;",
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(_OAUTH_ERROR_HTML.format(icon="&#x26A0;", error="Cloud backup module not available"))
479
+ return HTMLResponse(_oauth_error_page(icon="&#x26A0;", 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(_OAUTH_ERROR_HTML.format(icon="&#x274C;", error=f"GitHub denied access: {error}"))
579
+ return HTMLResponse(_oauth_error_page(icon="&#x274C;", error=f"GitHub denied access: {error}"))
557
580
 
558
581
  if not code:
559
- return HTMLResponse(_OAUTH_ERROR_HTML.format(icon="&#x274C;", error="No authorization code received"))
582
+ return HTMLResponse(_oauth_error_page(icon="&#x274C;", 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(_OAUTH_ERROR_HTML.format(icon="&#x274C;", error="GitHub OAuth App not configured"))
591
+ return HTMLResponse(_oauth_error_page(icon="&#x274C;", 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(_OAUTH_ERROR_HTML.format(icon="&#x274C;", error=data.get("error_description", "Failed to get access token")))
604
+ return HTMLResponse(_oauth_error_page(icon="&#x274C;", 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(_OAUTH_ERROR_HTML.format(icon="&#x274C;", error=result["error"]))
609
+ return HTMLResponse(_oauth_error_page(icon="&#x274C;", error=result["error"]))
587
610
 
588
- return HTMLResponse(_OAUTH_SUCCESS_HTML.format(
611
+ return HTMLResponse(_oauth_success_page(
589
612
  icon="&#x2705;",
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(_OAUTH_ERROR_HTML.format(icon="&#x274C;", error=str(exc)))
618
+ return HTMLResponse(_oauth_error_page(icon="&#x274C;", 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 action outcome for behavioral learning.
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
- tracker = OutcomeTracker(str(LEARNING_DB))
126
-
127
- context_dict = {"note": context_note} if context_note else {}
128
- row_id = tracker.record_outcome(
129
- memory_ids=memory_ids,
130
- outcome=outcome,
131
- action_type=action_type,
132
- context=context_dict,
133
- project=profile,
134
- )
135
-
136
- if row_id is None:
137
- return {"success": False, "error": "Failed to record outcome"}
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": row_id,
167
+ "success": True, "outcome_id": outcome_id,
141
168
  "active_profile": profile,
142
- "message": f"Recorded {outcome} outcome for {len(memory_ids)} memories",
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)