superlocalmemory 4.0.4 → 4.0.6

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 (90) hide show
  1. package/CHANGELOG.md +90 -0
  2. package/README.md +23 -14
  3. package/ide/configs/codex-mcp.toml +2 -2
  4. package/package.json +3 -1
  5. package/plugin/.claude-plugin/plugin.json +1 -1
  6. package/plugin/.mcp.json +1 -0
  7. package/plugin/CLAUDE.md +3 -3
  8. package/plugin/agents/slm-governance-advisor.md +1 -1
  9. package/plugin/agents/slm-loop-runner.md +1 -1
  10. package/plugin/agents/slm-memory-advisor.md +1 -1
  11. package/plugin/agents/slm-optimize-advisor.md +1 -1
  12. package/plugin/requirements.txt +1 -1
  13. package/plugin/skills/slm-cache/SKILL.md +1 -1
  14. package/plugin/skills/slm-compress/SKILL.md +1 -1
  15. package/plugin/skills/slm-governance/SKILL.md +1 -1
  16. package/plugin/skills/slm-graph/SKILL.md +3 -2
  17. package/plugin/skills/slm-loop/SKILL.md +1 -1
  18. package/plugin/skills/slm-mesh/SKILL.md +1 -1
  19. package/plugin/skills/slm-profile/SKILL.md +2 -1
  20. package/plugin/skills/slm-recall/SKILL.md +1 -1
  21. package/plugin/skills/slm-remember/SKILL.md +1 -1
  22. package/plugin/skills/slm-scope/SKILL.md +1 -1
  23. package/plugin/skills/slm-session/SKILL.md +1 -1
  24. package/plugin/skills/slm-status/SKILL.md +1 -1
  25. package/plugin-src/rules/AGENTS.md +6 -5
  26. package/plugin-src/skills/slm-graph/SKILL.md +2 -1
  27. package/plugin-src/skills/slm-profile/SKILL.md +1 -0
  28. package/pyproject.toml +1 -1
  29. package/src/superlocalmemory/__init__.py +1 -1
  30. package/src/superlocalmemory/access/rbac.py +106 -0
  31. package/src/superlocalmemory/brain/__init__.py +5 -0
  32. package/src/superlocalmemory/brain/truth.py +418 -0
  33. package/src/superlocalmemory/cli/__main__.py +17 -0
  34. package/src/superlocalmemory/cli/commands.py +96 -28
  35. package/src/superlocalmemory/cli/gdpr_cmd.py +779 -0
  36. package/src/superlocalmemory/cli/gdpr_io.py +109 -0
  37. package/src/superlocalmemory/cli/main.py +88 -0
  38. package/src/superlocalmemory/code_graph/extractors/__init__.py +17 -0
  39. package/src/superlocalmemory/code_graph/graph_store.py +180 -3
  40. package/src/superlocalmemory/code_graph/parser.py +280 -100
  41. package/src/superlocalmemory/compliance/gdpr.py +358 -0
  42. package/src/superlocalmemory/core/config.py +44 -1
  43. package/src/superlocalmemory/core/context_cache.py +58 -1
  44. package/src/superlocalmemory/core/engine_wiring.py +5 -1
  45. package/src/superlocalmemory/core/maintenance.py +43 -1
  46. package/src/superlocalmemory/core/mutations.py +155 -25
  47. package/src/superlocalmemory/core/recall_pipeline.py +6 -10
  48. package/src/superlocalmemory/core/recall_worker.py +33 -12
  49. package/src/superlocalmemory/core/remember_runtime.py +271 -2
  50. package/src/superlocalmemory/core/store_pipeline.py +100 -38
  51. package/src/superlocalmemory/encoding/consolidator.py +17 -47
  52. package/src/superlocalmemory/encoding/temporal_validator.py +14 -18
  53. package/src/superlocalmemory/hooks/user_prompt_hook.py +1 -1
  54. package/src/superlocalmemory/infra/backup.py +138 -0
  55. package/src/superlocalmemory/infra/backup_obligations.py +423 -0
  56. package/src/superlocalmemory/integrations/bounded_loops_mcp.py +4 -3
  57. package/src/superlocalmemory/learning/engagement.py +165 -0
  58. package/src/superlocalmemory/mcp/profiles.py +19 -7
  59. package/src/superlocalmemory/mcp/server.py +4 -2
  60. package/src/superlocalmemory/mcp/tools_brain.py +54 -10
  61. package/src/superlocalmemory/mcp/tools_code_graph.py +31 -4
  62. package/src/superlocalmemory/mcp/tools_core.py +88 -3
  63. package/src/superlocalmemory/mcp/tools_v3.py +20 -6
  64. package/src/superlocalmemory/retrieval/engine.py +28 -10
  65. package/src/superlocalmemory/retrieval/remote_reranker.py +108 -11
  66. package/src/superlocalmemory/retrieval/temporal_validity_filter.py +119 -19
  67. package/src/superlocalmemory/server/routes/brain.py +297 -14
  68. package/src/superlocalmemory/server/routes/learning.py +13 -25
  69. package/src/superlocalmemory/server/routes/memories.py +129 -3
  70. package/src/superlocalmemory/server/routes/v3_api.py +171 -60
  71. package/src/superlocalmemory/storage/_migration_internals.py +4 -0
  72. package/src/superlocalmemory/storage/_schema_version.py +2 -2
  73. package/src/superlocalmemory/storage/correction_cases.py +670 -0
  74. package/src/superlocalmemory/storage/database.py +230 -24
  75. package/src/superlocalmemory/storage/migration_runner.py +7 -0
  76. package/src/superlocalmemory/storage/migrations/M042_correction_case_ledger.py +245 -0
  77. package/src/superlocalmemory/storage/migrations/__init__.py +2 -0
  78. package/src/superlocalmemory/storage/models.py +12 -4
  79. package/src/superlocalmemory/storage/write_coordinator.py +4 -0
  80. package/src/superlocalmemory/summaries/__init__.py +37 -0
  81. package/src/superlocalmemory/summaries/base.py +108 -0
  82. package/src/superlocalmemory/summaries/daily_reflection.py +293 -0
  83. package/src/superlocalmemory/summaries/project_work_log.py +424 -0
  84. package/src/superlocalmemory/summaries/session_summary.py +307 -0
  85. package/src/superlocalmemory/ui/css/design-system.css +76 -1
  86. package/src/superlocalmemory/ui/index.html +28 -11
  87. package/src/superlocalmemory/ui/js/brain.js +43 -7
  88. package/src/superlocalmemory/ui/js/od-agents.js +49 -5
  89. package/src/superlocalmemory/ui/js/od-brain.js +280 -84
  90. package/src/superlocalmemory/ui/js/od-graph.js +147 -6
@@ -0,0 +1,108 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+ # Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
4
+
5
+ """Base types for the #113 bounded summary layer."""
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass, field
10
+ from typing import Any
11
+
12
+
13
+ @dataclass
14
+ class SummaryResult:
15
+ """A bounded, profile-scoped, traceable summary of user memories.
16
+
17
+ Maintainer's binding constraint (issue #113 reply):
18
+ "views must be customizable, profile-scoped, privacy-aware, and
19
+ traceable back to the underlying memories rather than becoming
20
+ opaque generic summaries"
21
+
22
+ This dataclass enforces three of those four constraints structurally:
23
+
24
+ Traceability
25
+ ``source_fact_ids`` carries the atomic_facts.fact_id for every fact
26
+ that contributed to this summary. A user can always drill back to
27
+ the raw memories.
28
+
29
+ Profile scope
30
+ ``profile_id`` is mandatory; callers must never mix profiles.
31
+
32
+ Honesty / non-opaqueness
33
+ ``coverage`` must be set to an accurate value. See the constants
34
+ below. A summary over 3.9% of facts that presents itself as "your
35
+ session" is precisely the opaque generic summary the maintainer said
36
+ to avoid.
37
+
38
+ Generated-by
39
+ ``generated_by`` records whether the content is extractive
40
+ (deterministic, always available, Mode A default) or came from an
41
+ LLM (Mode B Ollama / Mode C cloud).
42
+
43
+ Attributes:
44
+ kind: "session" | "daily" | "project"
45
+ profile_id: Owning profile — never expose across profiles.
46
+ content: Human-readable summary text.
47
+ source_fact_ids: IDs of the atomic_facts that contributed.
48
+ Empty only when the underlying data does not exist.
49
+ coverage: One of the COVERAGE_* constants below.
50
+ generated_by: One of the GENERATED_BY_* constants below.
51
+ metadata: Extra context: date, project_path, session_id, etc.
52
+ """
53
+
54
+ kind: str
55
+ profile_id: str
56
+ content: str
57
+ source_fact_ids: list[str]
58
+ coverage: str
59
+ generated_by: str
60
+ metadata: dict[str, Any] = field(default_factory=dict)
61
+
62
+
63
+ # ── coverage constants ──────────────────────────────────────────────────────
64
+ #
65
+ # Use these strings; the acceptance gate checks for their presence
66
+ # and the values must be human-interpretable without this file.
67
+
68
+ COVERAGE_FULL = "full"
69
+ """All relevant data was available and contributed to the summary."""
70
+
71
+ COVERAGE_PARTIAL = "partial"
72
+ """Some data was available. Session summaries are always at most partial
73
+ because only ~3.9% of facts carry a session_id on a real store."""
74
+
75
+ COVERAGE_INSUFFICIENT = "insufficient"
76
+ """Too few facts to produce a meaningful summary (below MIN_FACTS threshold)."""
77
+
78
+ COVERAGE_NO_SESSION = "no_session"
79
+ """Session ID not found, or the session has no associated facts."""
80
+
81
+ COVERAGE_UNAVAILABLE = "unavailable"
82
+ """Required data does not exist or a query error prevented access."""
83
+
84
+
85
+ # ── generated_by constants ──────────────────────────────────────────────────
86
+ #
87
+ # extractive is the deterministic fallback, ALWAYS available.
88
+ # Mode A users never get anything else. Mode B/C users fall back when
89
+ # Ollama or the cloud is down — silence is not an option.
90
+
91
+ GENERATED_BY_EXTRACTIVE = "extractive"
92
+ """Deterministic extractive summary — no LLM. Always available."""
93
+
94
+ GENERATED_BY_LLM_B = "llm_b"
95
+ """Ollama local LLM (Mode B). Falls back to extractive if unavailable."""
96
+
97
+ GENERATED_BY_LLM_C = "llm_c"
98
+ """Cloud LLM (Mode C). Falls back via llm_b to extractive."""
99
+
100
+
101
+ def get_mode_str(config: object | None) -> str:
102
+ """Extract the operating mode string ('a', 'b', or 'c') from a config."""
103
+ if config is None:
104
+ return "a"
105
+ m = getattr(config, "mode", None)
106
+ if m is None:
107
+ return "a"
108
+ return getattr(m, "value", str(m)).lower()
@@ -0,0 +1,293 @@
1
+ # Copyright (c) 2026 Varun Pratap Bhardwaj / Qualixar
2
+ # Licensed under AGPL-3.0-or-later - see LICENSE file
3
+ # Part of SuperLocalMemory V3 | https://qualixar.com | https://varunpratap.com
4
+
5
+ """Daily Reflection — issue #113 bounded summary.
6
+
7
+ Produces a human-readable summary of all facts recorded on a specific date,
8
+ grouped by time of day and sorted by importance.
9
+
10
+ MEASURED DATA REALITY
11
+ ---------------------
12
+ On a real 3,294-fact store there are 48 distinct active days.
13
+ Per-day volumes range from 4 to 454 facts. Daily reflections are
14
+ structurally viable for this store.
15
+
16
+ DETERMINISTIC FALLBACK
17
+ -----------------------
18
+ The extractive path groups facts by topic entities and summarises the top-N
19
+ by importance. It never calls an LLM and never fails to return a result.
20
+ Mode B/C enrichment is attempted when configured, but falls back to extractive
21
+ on any failure — silence is not acceptable.
22
+
23
+ HOT PATH EXCLUSION
24
+ -------------------
25
+ This module must NEVER be imported from core/recall_pipeline.py or
26
+ core/store_pipeline.py.
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import logging
32
+ import sqlite3
33
+ from datetime import date
34
+ from pathlib import Path
35
+
36
+ from .base import (
37
+ COVERAGE_FULL,
38
+ COVERAGE_INSUFFICIENT,
39
+ COVERAGE_UNAVAILABLE,
40
+ GENERATED_BY_EXTRACTIVE,
41
+ GENERATED_BY_LLM_B,
42
+ GENERATED_BY_LLM_C,
43
+ SummaryResult,
44
+ get_mode_str,
45
+ )
46
+
47
+ logger = logging.getLogger("superlocalmemory.summaries.daily")
48
+
49
+ _MIN_FACTS = 2 # Below this threshold, coverage=insufficient
50
+ _BODY_FACTS = 10 # Top-N facts shown in extractive summary
51
+ _MAX_FACT_CHARS = 300 # Per-fact character cap in the body
52
+
53
+
54
+ def generate_daily_reflection(
55
+ db_path: str | Path,
56
+ target_date: str | date,
57
+ profile_id: str = "default",
58
+ config: object | None = None,
59
+ ) -> SummaryResult:
60
+ """Generate a Daily Reflection for a specific date.
61
+
62
+ Args:
63
+ db_path: Path to memory.db.
64
+ target_date: The date to reflect on. Accepts ``date`` objects or
65
+ ISO-format strings ("2026-08-17").
66
+ profile_id: Profile scope — never mix profiles.
67
+ config: Optional SLMConfig for LLM enrichment. None = Mode A
68
+ (extractive, always deterministic).
69
+
70
+ Returns:
71
+ SummaryResult with source_fact_ids for every contributing fact.
72
+ Never returns None.
73
+ """
74
+ db_path = Path(db_path)
75
+ date_str = target_date.isoformat() if isinstance(target_date, date) else str(target_date)
76
+
77
+ # ── query ────────────────────────────────────────────────────────────────
78
+ try:
79
+ conn = sqlite3.connect(str(db_path), timeout=5.0)
80
+ conn.row_factory = sqlite3.Row
81
+ conn.execute("PRAGMA query_only=ON")
82
+ try:
83
+ rows = conn.execute(
84
+ """
85
+ SELECT fact_id, content, created_at, importance,
86
+ canonical_entities_json, lifecycle
87
+ FROM atomic_facts
88
+ WHERE profile_id = ?
89
+ AND DATE(created_at) = ?
90
+ AND lifecycle != 'archived'
91
+ ORDER BY importance DESC, created_at ASC
92
+ """,
93
+ (profile_id, date_str),
94
+ ).fetchall()
95
+ finally:
96
+ conn.close()
97
+ except Exception as exc:
98
+ logger.warning("daily reflection query failed for %s: %s", date_str, exc)
99
+ return SummaryResult(
100
+ kind="daily",
101
+ profile_id=profile_id,
102
+ content=f"Daily reflection for {date_str} is unavailable: data access error.",
103
+ source_fact_ids=[],
104
+ coverage=COVERAGE_UNAVAILABLE,
105
+ generated_by=GENERATED_BY_EXTRACTIVE,
106
+ metadata={"date": date_str, "error": str(exc)},
107
+ )
108
+
109
+ # ── coverage decision ─────────────────────────────────────────────────────
110
+ if not rows:
111
+ return SummaryResult(
112
+ kind="daily",
113
+ profile_id=profile_id,
114
+ content=f"No facts recorded on {date_str}.",
115
+ source_fact_ids=[],
116
+ coverage=COVERAGE_INSUFFICIENT,
117
+ generated_by=GENERATED_BY_EXTRACTIVE,
118
+ metadata={"date": date_str, "fact_count": 0},
119
+ )
120
+
121
+ facts = [dict(r) for r in rows]
122
+ source_fact_ids = [f["fact_id"] for f in facts]
123
+ fact_count = len(facts)
124
+
125
+ if fact_count < _MIN_FACTS:
126
+ coverage = COVERAGE_INSUFFICIENT
127
+ else:
128
+ coverage = COVERAGE_FULL
129
+
130
+ # ── extractive summary (deterministic, always available) ──────────────────
131
+ extractive_content = _build_extractive_content(date_str, facts, fact_count)
132
+
133
+ # ── LLM enrichment (optional) ─────────────────────────────────────────────
134
+ mode = get_mode_str(config)
135
+ if fact_count >= _MIN_FACTS and mode in ("b", "c"):
136
+ llm_content, llm_mode = _try_llm(date_str, facts, config, mode)
137
+ if llm_content:
138
+ return SummaryResult(
139
+ kind="daily",
140
+ profile_id=profile_id,
141
+ content=llm_content,
142
+ source_fact_ids=source_fact_ids,
143
+ coverage=coverage,
144
+ generated_by=llm_mode,
145
+ metadata={"date": date_str, "fact_count": fact_count},
146
+ )
147
+
148
+ return SummaryResult(
149
+ kind="daily",
150
+ profile_id=profile_id,
151
+ content=extractive_content,
152
+ source_fact_ids=source_fact_ids,
153
+ coverage=coverage,
154
+ generated_by=GENERATED_BY_EXTRACTIVE,
155
+ metadata={"date": date_str, "fact_count": fact_count},
156
+ )
157
+
158
+
159
+ # ── helpers ──────────────────────────────────────────────────────────────────
160
+
161
+ def _build_extractive_content(
162
+ date_str: str,
163
+ facts: list[dict],
164
+ fact_count: int,
165
+ ) -> str:
166
+ """Build a deterministic extractive daily reflection."""
167
+ import json
168
+
169
+ lines = [
170
+ f"Daily reflection: {date_str}",
171
+ f"Total facts recorded: {fact_count}",
172
+ "",
173
+ "Highlights:",
174
+ ]
175
+ for f in facts[:_BODY_FACTS]:
176
+ content = f.get("content", "")
177
+ if len(content) > _MAX_FACT_CHARS:
178
+ content = content[:_MAX_FACT_CHARS - 3] + "..."
179
+ lines.append(f" - {content}")
180
+ if fact_count > _BODY_FACTS:
181
+ lines.append(f" ... and {fact_count - _BODY_FACTS} additional facts.")
182
+
183
+ # Entity summary: which entities appeared most
184
+ entity_counts: dict[str, int] = {}
185
+ for f in facts:
186
+ raw = f.get("canonical_entities_json") or "[]"
187
+ try:
188
+ entities = json.loads(raw)
189
+ except (ValueError, TypeError):
190
+ entities = []
191
+ for e in entities:
192
+ entity_counts[e] = entity_counts.get(e, 0) + 1
193
+
194
+ if entity_counts:
195
+ top_entities = sorted(entity_counts.items(), key=lambda x: x[1], reverse=True)[:5]
196
+ entity_str = ", ".join(f"{e} ({c})" for e, c in top_entities)
197
+ lines.append("")
198
+ lines.append(f"Active entities: {entity_str}")
199
+
200
+ return "\n".join(lines)
201
+
202
+
203
+ def _try_llm(
204
+ date_str: str,
205
+ facts: list[dict],
206
+ config: object | None,
207
+ mode: str,
208
+ ) -> tuple[str | None, str]:
209
+ """Attempt LLM-based daily reflection. Returns (content, generated_by)."""
210
+ prompt = (
211
+ f"Write a concise daily reflection for {date_str} based on these facts. "
212
+ f"Focus on main themes and accomplishments in 3-5 sentences."
213
+ )
214
+ if mode == "c":
215
+ result = _call_cloud_llm(prompt, facts, config)
216
+ if result:
217
+ return result, GENERATED_BY_LLM_C
218
+ if mode in ("b", "c"):
219
+ result = _call_ollama(prompt, facts, config)
220
+ if result:
221
+ return result, GENERATED_BY_LLM_B
222
+ return None, GENERATED_BY_EXTRACTIVE
223
+
224
+
225
+ def _call_ollama(
226
+ prompt: str,
227
+ facts: list[dict],
228
+ config: object | None,
229
+ ) -> str | None:
230
+ """Mode B: call Ollama. Returns None on any failure (extractive fallback)."""
231
+ try:
232
+ import json
233
+ import urllib.request
234
+
235
+ api_base = "http://localhost:11434"
236
+ model = "llama3.2"
237
+ timeout = 30
238
+ if config and hasattr(config, "llm"):
239
+ api_base = getattr(config.llm, "api_base", api_base) or api_base
240
+ model = getattr(config.llm, "model", model) or model
241
+ timeout = (
242
+ getattr(config.llm, "timeout_seconds", None)
243
+ or getattr(config.llm, "timeout", None)
244
+ or timeout
245
+ )
246
+
247
+ fact_texts = "\n".join(f"- {f['content']}" for f in facts[:15])
248
+ full_prompt = f"{prompt}\n\nFacts from {len(facts)} recorded:\n{fact_texts}"
249
+ payload = json.dumps({
250
+ "model": model,
251
+ "prompt": full_prompt,
252
+ "stream": False,
253
+ "options": {"num_predict": 300},
254
+ }).encode()
255
+ req = urllib.request.Request(
256
+ f"{api_base}/api/generate",
257
+ data=payload,
258
+ headers={"Content-Type": "application/json"},
259
+ )
260
+ resp = urllib.request.urlopen(req, timeout=timeout)
261
+ data = json.loads(resp.read().decode())
262
+ text = data.get("response", "").strip()
263
+ return text if text and len(text) > 20 else None
264
+ except Exception as exc:
265
+ logger.debug("Ollama daily reflection failed: %s", exc)
266
+ return None
267
+
268
+
269
+ def _call_cloud_llm(
270
+ prompt: str,
271
+ facts: list[dict],
272
+ config: object | None,
273
+ ) -> str | None:
274
+ """Mode C: call the configured cloud LLM. Returns None on any failure."""
275
+ if not config or not hasattr(config, "llm"):
276
+ return None
277
+ try:
278
+ from superlocalmemory.llm.backbone import LLMBackbone
279
+ llm = LLMBackbone(config.llm)
280
+ if not llm.is_available():
281
+ return None
282
+ fact_texts = "\n".join(f"- {f['content']}" for f in facts[:15])
283
+ full_prompt = f"{prompt}\n\nFacts:\n{fact_texts}"
284
+ text = llm.generate(
285
+ prompt=full_prompt,
286
+ system="You are a concise personal memory summariser.",
287
+ max_tokens=300,
288
+ temperature=0.1,
289
+ )
290
+ return text.strip() if text and len(text.strip()) > 20 else None
291
+ except Exception as exc:
292
+ logger.debug("Cloud LLM daily reflection failed: %s", exc)
293
+ return None