superlocalmemory 4.0.5 → 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.
- package/CHANGELOG.md +57 -0
- package/README.md +10 -6
- package/package.json +3 -1
- package/pyproject.toml +1 -1
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/access/rbac.py +106 -0
- package/src/superlocalmemory/brain/truth.py +80 -10
- package/src/superlocalmemory/cli/__main__.py +17 -0
- package/src/superlocalmemory/cli/commands.py +14 -3
- package/src/superlocalmemory/cli/gdpr_cmd.py +779 -0
- package/src/superlocalmemory/cli/gdpr_io.py +109 -0
- package/src/superlocalmemory/cli/main.py +76 -0
- package/src/superlocalmemory/code_graph/extractors/__init__.py +17 -0
- package/src/superlocalmemory/code_graph/graph_store.py +180 -3
- package/src/superlocalmemory/code_graph/parser.py +280 -100
- package/src/superlocalmemory/compliance/gdpr.py +358 -0
- package/src/superlocalmemory/core/config.py +44 -1
- package/src/superlocalmemory/core/engine_wiring.py +5 -1
- package/src/superlocalmemory/core/maintenance.py +43 -1
- package/src/superlocalmemory/core/recall_worker.py +33 -12
- package/src/superlocalmemory/infra/backup.py +138 -0
- package/src/superlocalmemory/infra/backup_obligations.py +423 -0
- package/src/superlocalmemory/learning/engagement.py +165 -0
- package/src/superlocalmemory/mcp/tools_code_graph.py +31 -4
- package/src/superlocalmemory/mcp/tools_v3.py +20 -6
- package/src/superlocalmemory/retrieval/engine.py +21 -0
- package/src/superlocalmemory/retrieval/remote_reranker.py +108 -11
- package/src/superlocalmemory/server/routes/brain.py +283 -15
- package/src/superlocalmemory/server/routes/learning.py +13 -25
- package/src/superlocalmemory/server/routes/v3_api.py +171 -60
- package/src/superlocalmemory/storage/database.py +36 -0
- package/src/superlocalmemory/storage/models.py +12 -4
- package/src/superlocalmemory/summaries/__init__.py +37 -0
- package/src/superlocalmemory/summaries/base.py +108 -0
- package/src/superlocalmemory/summaries/daily_reflection.py +293 -0
- package/src/superlocalmemory/summaries/project_work_log.py +424 -0
- package/src/superlocalmemory/summaries/session_summary.py +307 -0
- package/src/superlocalmemory/ui/css/design-system.css +76 -1
- package/src/superlocalmemory/ui/index.html +28 -11
- package/src/superlocalmemory/ui/js/od-agents.js +49 -5
- package/src/superlocalmemory/ui/js/od-brain.js +257 -77
- package/src/superlocalmemory/ui/js/od-graph.js +147 -6
|
@@ -0,0 +1,307 @@
|
|
|
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
|
+
"""Session Summary — issue #113 bounded summary.
|
|
6
|
+
|
|
7
|
+
CRITICAL DATA HONESTY NOTE
|
|
8
|
+
---------------------------
|
|
9
|
+
On a real 3,294-fact store, only 127 facts (3.9%) carry a session_id.
|
|
10
|
+
A Session Summary that presents itself as "everything you did this session"
|
|
11
|
+
while silently covering 4% of the facts is the same overclaiming Wave 4
|
|
12
|
+
removed from brain/truth.py.
|
|
13
|
+
|
|
14
|
+
Coverage is ALWAYS disclosed. If the session has too few facts to summarise
|
|
15
|
+
meaningfully we say so; we never silently return partial data as complete.
|
|
16
|
+
|
|
17
|
+
DETERMINISTIC FALLBACK IS MANDATORY
|
|
18
|
+
-------------------------------------
|
|
19
|
+
Mode A users have no LLM at all. Mode B/C users lose theirs whenever Ollama
|
|
20
|
+
or the network is down. Every call to generate_session_summary() MUST return
|
|
21
|
+
a SummaryResult. It may say the data is insufficient; it may never return
|
|
22
|
+
None or an empty result. The extractive fallback path is always active.
|
|
23
|
+
|
|
24
|
+
HOT PATH EXCLUSION
|
|
25
|
+
-------------------
|
|
26
|
+
This module must NEVER be imported from core/recall_pipeline.py or
|
|
27
|
+
core/store_pipeline.py. Summary generation performs multi-table reads and
|
|
28
|
+
optional LLM calls. It belongs in background maintenance or on explicit
|
|
29
|
+
user request only.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import logging
|
|
35
|
+
import sqlite3
|
|
36
|
+
from datetime import date, timezone
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
|
|
39
|
+
from .base import (
|
|
40
|
+
COVERAGE_FULL,
|
|
41
|
+
COVERAGE_INSUFFICIENT,
|
|
42
|
+
COVERAGE_NO_SESSION,
|
|
43
|
+
COVERAGE_PARTIAL,
|
|
44
|
+
COVERAGE_UNAVAILABLE,
|
|
45
|
+
GENERATED_BY_EXTRACTIVE,
|
|
46
|
+
GENERATED_BY_LLM_B,
|
|
47
|
+
GENERATED_BY_LLM_C,
|
|
48
|
+
SummaryResult,
|
|
49
|
+
get_mode_str,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
logger = logging.getLogger("superlocalmemory.summaries.session")
|
|
53
|
+
|
|
54
|
+
# Minimum number of session facts to attempt a meaningful summary.
|
|
55
|
+
# Below this threshold we return coverage=insufficient and a stub.
|
|
56
|
+
_MIN_FACTS = 3
|
|
57
|
+
|
|
58
|
+
# Maximum facts to include in the content body (extractive mode).
|
|
59
|
+
_BODY_FACTS = 7
|
|
60
|
+
|
|
61
|
+
# Maximum character length for a single fact in the body.
|
|
62
|
+
_MAX_FACT_CHARS = 250
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def generate_session_summary(
|
|
66
|
+
db_path: str | Path,
|
|
67
|
+
session_id: str,
|
|
68
|
+
profile_id: str = "default",
|
|
69
|
+
config: object | None = None,
|
|
70
|
+
) -> SummaryResult:
|
|
71
|
+
"""Generate a Session Summary for a specific session.
|
|
72
|
+
|
|
73
|
+
COVERAGE DISCLOSURE: session data is sparse on real stores (~3.9% of
|
|
74
|
+
facts carry a session_id). The returned SummaryResult.coverage is always
|
|
75
|
+
set to an accurate value — never "full" unless the session is truly
|
|
76
|
+
complete.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
db_path: Path to memory.db.
|
|
80
|
+
session_id: The session identifier (atomic_facts.session_id).
|
|
81
|
+
profile_id: Profile scope — never mix profiles.
|
|
82
|
+
config: Optional SLMConfig for LLM mode (B/C). None = Mode A
|
|
83
|
+
(extractive only, always deterministic).
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
SummaryResult with source_fact_ids for every contributing fact.
|
|
87
|
+
Never returns None.
|
|
88
|
+
|
|
89
|
+
Note:
|
|
90
|
+
This function is NOT on the hot path. It belongs in background
|
|
91
|
+
maintenance or on explicit user request, never inside recall or store.
|
|
92
|
+
"""
|
|
93
|
+
db_path = Path(db_path)
|
|
94
|
+
|
|
95
|
+
# ── query ────────────────────────────────────────────────────────────────
|
|
96
|
+
try:
|
|
97
|
+
conn = sqlite3.connect(str(db_path), timeout=5.0)
|
|
98
|
+
conn.row_factory = sqlite3.Row
|
|
99
|
+
conn.execute("PRAGMA query_only=ON")
|
|
100
|
+
try:
|
|
101
|
+
rows = conn.execute(
|
|
102
|
+
"""
|
|
103
|
+
SELECT fact_id, content, created_at, importance, lifecycle
|
|
104
|
+
FROM atomic_facts
|
|
105
|
+
WHERE profile_id = ?
|
|
106
|
+
AND session_id = ?
|
|
107
|
+
AND lifecycle != 'archived'
|
|
108
|
+
ORDER BY importance DESC, created_at ASC
|
|
109
|
+
""",
|
|
110
|
+
(profile_id, session_id),
|
|
111
|
+
).fetchall()
|
|
112
|
+
finally:
|
|
113
|
+
conn.close()
|
|
114
|
+
except Exception as exc:
|
|
115
|
+
logger.warning("session summary query failed for %s: %s", session_id, exc)
|
|
116
|
+
return SummaryResult(
|
|
117
|
+
kind="session",
|
|
118
|
+
profile_id=profile_id,
|
|
119
|
+
content=(
|
|
120
|
+
f"Session summary for '{session_id}' is unavailable: "
|
|
121
|
+
f"data access error."
|
|
122
|
+
),
|
|
123
|
+
source_fact_ids=[],
|
|
124
|
+
coverage=COVERAGE_UNAVAILABLE,
|
|
125
|
+
generated_by=GENERATED_BY_EXTRACTIVE,
|
|
126
|
+
metadata={"session_id": session_id, "error": str(exc)},
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# ── coverage decision ─────────────────────────────────────────────────────
|
|
130
|
+
if not rows:
|
|
131
|
+
return SummaryResult(
|
|
132
|
+
kind="session",
|
|
133
|
+
profile_id=profile_id,
|
|
134
|
+
content=(
|
|
135
|
+
f"No facts found for session '{session_id}'. "
|
|
136
|
+
f"Note: only a small fraction of facts carry a session_id "
|
|
137
|
+
f"on the current store — coverage is inherently partial."
|
|
138
|
+
),
|
|
139
|
+
source_fact_ids=[],
|
|
140
|
+
coverage=COVERAGE_NO_SESSION,
|
|
141
|
+
generated_by=GENERATED_BY_EXTRACTIVE,
|
|
142
|
+
metadata={"session_id": session_id, "fact_count": 0},
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
facts = [dict(r) for r in rows]
|
|
146
|
+
source_fact_ids = [f["fact_id"] for f in facts]
|
|
147
|
+
fact_count = len(facts)
|
|
148
|
+
|
|
149
|
+
if fact_count < _MIN_FACTS:
|
|
150
|
+
coverage = COVERAGE_INSUFFICIENT
|
|
151
|
+
else:
|
|
152
|
+
# Session summaries are structurally partial: they cover only the facts
|
|
153
|
+
# that happened to carry a session_id. On a real store that is ~3.9%
|
|
154
|
+
# of the corpus. Reporting "full" would be dishonest.
|
|
155
|
+
coverage = COVERAGE_PARTIAL
|
|
156
|
+
|
|
157
|
+
# ── extractive summary (deterministic, always available) ──────────────────
|
|
158
|
+
extractive_content = _build_extractive_content(session_id, facts, fact_count)
|
|
159
|
+
|
|
160
|
+
# ── LLM enrichment (optional) ─────────────────────────────────────────────
|
|
161
|
+
mode = get_mode_str(config)
|
|
162
|
+
if fact_count >= _MIN_FACTS and mode in ("b", "c"):
|
|
163
|
+
llm_content, llm_mode = _try_llm(
|
|
164
|
+
f"Summarise the key activities in session {session_id}",
|
|
165
|
+
facts,
|
|
166
|
+
config,
|
|
167
|
+
mode,
|
|
168
|
+
)
|
|
169
|
+
if llm_content:
|
|
170
|
+
return SummaryResult(
|
|
171
|
+
kind="session",
|
|
172
|
+
profile_id=profile_id,
|
|
173
|
+
content=llm_content,
|
|
174
|
+
source_fact_ids=source_fact_ids,
|
|
175
|
+
coverage=coverage,
|
|
176
|
+
generated_by=llm_mode,
|
|
177
|
+
metadata={"session_id": session_id, "fact_count": fact_count},
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
return SummaryResult(
|
|
181
|
+
kind="session",
|
|
182
|
+
profile_id=profile_id,
|
|
183
|
+
content=extractive_content,
|
|
184
|
+
source_fact_ids=source_fact_ids,
|
|
185
|
+
coverage=coverage,
|
|
186
|
+
generated_by=GENERATED_BY_EXTRACTIVE,
|
|
187
|
+
metadata={"session_id": session_id, "fact_count": fact_count},
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# ── helpers ──────────────────────────────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
def _build_extractive_content(
|
|
194
|
+
session_id: str,
|
|
195
|
+
facts: list[dict],
|
|
196
|
+
fact_count: int,
|
|
197
|
+
) -> str:
|
|
198
|
+
"""Build a deterministic extractive summary from session facts.
|
|
199
|
+
|
|
200
|
+
Coverage is disclosed in the header. Top facts by importance are listed.
|
|
201
|
+
This is the fallback path — it must never fail or return an empty string.
|
|
202
|
+
"""
|
|
203
|
+
lines = [
|
|
204
|
+
f"Session: {session_id}",
|
|
205
|
+
f"Facts recorded: {fact_count}",
|
|
206
|
+
f"Coverage: partial (session facts are a subset of the full store)",
|
|
207
|
+
"",
|
|
208
|
+
"Top recorded facts:",
|
|
209
|
+
]
|
|
210
|
+
for f in facts[:_BODY_FACTS]:
|
|
211
|
+
content = f.get("content", "")
|
|
212
|
+
if len(content) > _MAX_FACT_CHARS:
|
|
213
|
+
content = content[:_MAX_FACT_CHARS - 3] + "..."
|
|
214
|
+
lines.append(f" - {content}")
|
|
215
|
+
if fact_count > _BODY_FACTS:
|
|
216
|
+
lines.append(f" ... and {fact_count - _BODY_FACTS} more facts.")
|
|
217
|
+
return "\n".join(lines)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _try_llm(
|
|
221
|
+
prompt: str,
|
|
222
|
+
facts: list[dict],
|
|
223
|
+
config: object | None,
|
|
224
|
+
mode: str,
|
|
225
|
+
) -> tuple[str | None, str]:
|
|
226
|
+
"""Attempt LLM summarisation. Returns (content, generated_by) or (None, extractive)."""
|
|
227
|
+
if mode == "c":
|
|
228
|
+
result = _call_cloud_llm(prompt, facts, config)
|
|
229
|
+
if result:
|
|
230
|
+
return result, GENERATED_BY_LLM_C
|
|
231
|
+
# fall through to Mode B
|
|
232
|
+
if mode in ("b", "c"):
|
|
233
|
+
result = _call_ollama(prompt, facts, config)
|
|
234
|
+
if result:
|
|
235
|
+
return result, GENERATED_BY_LLM_B
|
|
236
|
+
return None, GENERATED_BY_EXTRACTIVE
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _call_ollama(
|
|
240
|
+
prompt: str,
|
|
241
|
+
facts: list[dict],
|
|
242
|
+
config: object | None,
|
|
243
|
+
) -> str | None:
|
|
244
|
+
"""Mode B: call Ollama. Returns None on any failure."""
|
|
245
|
+
try:
|
|
246
|
+
import json
|
|
247
|
+
import urllib.request
|
|
248
|
+
|
|
249
|
+
api_base = "http://localhost:11434"
|
|
250
|
+
model = "llama3.2"
|
|
251
|
+
timeout = 30
|
|
252
|
+
if config and hasattr(config, "llm"):
|
|
253
|
+
api_base = getattr(config.llm, "api_base", api_base) or api_base
|
|
254
|
+
model = getattr(config.llm, "model", model) or model
|
|
255
|
+
timeout = (
|
|
256
|
+
getattr(config.llm, "timeout_seconds", None)
|
|
257
|
+
or getattr(config.llm, "timeout", None)
|
|
258
|
+
or timeout
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
fact_texts = "\n".join(f"- {f['content']}" for f in facts[:10])
|
|
262
|
+
full_prompt = f"{prompt}\n\nFacts:\n{fact_texts}\n\nRespond in 2-4 sentences."
|
|
263
|
+
payload = json.dumps({
|
|
264
|
+
"model": model,
|
|
265
|
+
"prompt": full_prompt,
|
|
266
|
+
"stream": False,
|
|
267
|
+
"options": {"num_predict": 200},
|
|
268
|
+
}).encode()
|
|
269
|
+
req = urllib.request.Request(
|
|
270
|
+
f"{api_base}/api/generate",
|
|
271
|
+
data=payload,
|
|
272
|
+
headers={"Content-Type": "application/json"},
|
|
273
|
+
)
|
|
274
|
+
resp = urllib.request.urlopen(req, timeout=timeout)
|
|
275
|
+
data = json.loads(resp.read().decode())
|
|
276
|
+
text = data.get("response", "").strip()
|
|
277
|
+
return text if text and len(text) > 20 else None
|
|
278
|
+
except Exception as exc:
|
|
279
|
+
logger.debug("Ollama session summary failed: %s", exc)
|
|
280
|
+
return None
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _call_cloud_llm(
|
|
284
|
+
prompt: str,
|
|
285
|
+
facts: list[dict],
|
|
286
|
+
config: object | None,
|
|
287
|
+
) -> str | None:
|
|
288
|
+
"""Mode C: call the configured cloud LLM. Returns None on any failure."""
|
|
289
|
+
if not config or not hasattr(config, "llm"):
|
|
290
|
+
return None
|
|
291
|
+
try:
|
|
292
|
+
from superlocalmemory.llm.backbone import LLMBackbone
|
|
293
|
+
llm = LLMBackbone(config.llm)
|
|
294
|
+
if not llm.is_available():
|
|
295
|
+
return None
|
|
296
|
+
fact_texts = "\n".join(f"- {f['content']}" for f in facts[:10])
|
|
297
|
+
full_prompt = f"{prompt}\n\nFacts:\n{fact_texts}\n\nRespond in 2-4 sentences."
|
|
298
|
+
text = llm.generate(
|
|
299
|
+
prompt=full_prompt,
|
|
300
|
+
system="You are a concise memory summariser.",
|
|
301
|
+
max_tokens=200,
|
|
302
|
+
temperature=0.1,
|
|
303
|
+
)
|
|
304
|
+
return text.strip() if text and len(text.strip()) > 20 else None
|
|
305
|
+
except Exception as exc:
|
|
306
|
+
logger.debug("Cloud LLM session summary failed: %s", exc)
|
|
307
|
+
return None
|
|
@@ -135,6 +135,12 @@ a:hover { text-decoration: underline; }
|
|
|
135
135
|
button { font-family: inherit; }
|
|
136
136
|
::selection { background: var(--violet-soft); }
|
|
137
137
|
.mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
|
|
138
|
+
/* Monospace values are usually paths, ids or hashes — reference data, not
|
|
139
|
+
prose. Left unbounded they set the width of whatever contains them:
|
|
140
|
+
#dashboard-basedir alone rendered 219px and pushed the Dashboard pane past
|
|
141
|
+
its own right edge. Clip instead of overflowing; the full value stays
|
|
142
|
+
available via the title attribute wherever these are rendered. */
|
|
143
|
+
.mono { min-width: 0; max-width: 100%; overflow: hidden; text-overflow: ellipsis; }
|
|
138
144
|
.num { font-variant-numeric: tabular-nums; }
|
|
139
145
|
.muted { color: var(--fg-2); }
|
|
140
146
|
.dim { color: var(--fg-3); }
|
|
@@ -424,7 +430,15 @@ button { font-family: inherit; }
|
|
|
424
430
|
/* ============================================================
|
|
425
431
|
TABLES / LISTS
|
|
426
432
|
============================================================ */
|
|
427
|
-
|
|
433
|
+
/* A wide table scrolls INSIDE its own card at any width, not only on phones.
|
|
434
|
+
The ≤768px block already did exactly this; the rule was simply never applied
|
|
435
|
+
above that breakpoint, so between roughly 800px and 1200px a six-column
|
|
436
|
+
table (Memories renders 783px of columns) overflowed its pane instead. The
|
|
437
|
+
pane does technically scroll, but an inner scroll region with no visible
|
|
438
|
+
affordance reads as "the page is cut off on the right" — which is precisely
|
|
439
|
+
how it was reported. Keeping the scroll inside the card makes the clipped
|
|
440
|
+
edge belong to the table, where a horizontal scrollbar is expected. */
|
|
441
|
+
.tbl { width: 100%; border-collapse: collapse; font-size: 13px; display: block; overflow-x: auto; -webkit-overflow-scrolling: touch; max-width: 100%; }
|
|
428
442
|
.tbl th { text-align: left; font-size: 11px; text-transform: uppercase; letter-spacing: .06em; color: var(--fg-3); font-weight: 600; padding: 10px 14px; border-bottom: 1px solid var(--border); }
|
|
429
443
|
.tbl td { padding: 12px 14px; border-bottom: 1px solid var(--border); vertical-align: middle; }
|
|
430
444
|
.tbl tr:last-child td { border-bottom: 0; }
|
|
@@ -615,6 +629,67 @@ button { font-family: inherit; }
|
|
|
615
629
|
.kpi-strip { grid-template-columns: 1fr !important; }
|
|
616
630
|
}
|
|
617
631
|
|
|
632
|
+
/* ---- Knowledge-graph panel toggle (≤1100px only) -------------------------
|
|
633
|
+
#odg-panel-toggle: "Details & Chat ↓" floats at centre-bottom of the
|
|
634
|
+
graph stage. Tells the user the stacked inspector / Ask-your-memory panel
|
|
635
|
+
exists and scrolls it into view on click.
|
|
636
|
+
.odg-back-btn: companion at the top of the stacked inspector; scrolls back
|
|
637
|
+
to the graph stage.
|
|
638
|
+
Both are hidden by default. Only revealed at ≤1100px where the inspector
|
|
639
|
+
stacks below the stage instead of sitting beside it in the two-column grid.
|
|
640
|
+
The existing @media (max-width:1100px) block above is NOT modified —
|
|
641
|
+
these are additive rules for the new elements only. -------------------- */
|
|
642
|
+
#odg-panel-toggle,
|
|
643
|
+
.odg-back-btn { display: none; }
|
|
644
|
+
|
|
645
|
+
@media (max-width: 1100px) {
|
|
646
|
+
/* Floating affordance pinned to centre-bottom of the graph stage overlay */
|
|
647
|
+
#odg-panel-toggle {
|
|
648
|
+
display: inline-flex;
|
|
649
|
+
align-items: center;
|
|
650
|
+
gap: 6px;
|
|
651
|
+
position: absolute;
|
|
652
|
+
bottom: 16px;
|
|
653
|
+
left: 50%;
|
|
654
|
+
transform: translateX(-50%);
|
|
655
|
+
z-index: 10;
|
|
656
|
+
height: 36px;
|
|
657
|
+
padding: 0 20px;
|
|
658
|
+
border-radius: var(--r-pill);
|
|
659
|
+
border: 1px solid var(--border-strong);
|
|
660
|
+
background: var(--card);
|
|
661
|
+
color: var(--fg);
|
|
662
|
+
font-size: 13px;
|
|
663
|
+
font-weight: 600;
|
|
664
|
+
cursor: pointer;
|
|
665
|
+
box-shadow: var(--sh-md);
|
|
666
|
+
white-space: nowrap;
|
|
667
|
+
transition: background .15s, color .15s, border-color .15s;
|
|
668
|
+
}
|
|
669
|
+
#odg-panel-toggle:hover { background: var(--violet); color: var(--violet-fg); border-color: transparent; }
|
|
670
|
+
#odg-panel-toggle:focus-visible { outline: 2px solid var(--violet); outline-offset: 3px; }
|
|
671
|
+
|
|
672
|
+
/* Companion button at top of stacked inspector */
|
|
673
|
+
.odg-back-btn {
|
|
674
|
+
display: flex;
|
|
675
|
+
align-items: center;
|
|
676
|
+
justify-content: center;
|
|
677
|
+
gap: 6px;
|
|
678
|
+
width: 100%;
|
|
679
|
+
padding: 9px 16px;
|
|
680
|
+
border: 0;
|
|
681
|
+
border-bottom: 1px solid var(--border);
|
|
682
|
+
background: var(--card-2);
|
|
683
|
+
color: var(--fg-2);
|
|
684
|
+
font-size: 12.5px;
|
|
685
|
+
font-weight: 600;
|
|
686
|
+
cursor: pointer;
|
|
687
|
+
transition: background .15s, color .15s;
|
|
688
|
+
}
|
|
689
|
+
.odg-back-btn:hover { background: var(--violet-soft); color: var(--violet); }
|
|
690
|
+
.odg-back-btn:focus-visible { outline: 2px solid var(--violet); outline-offset: -2px; }
|
|
691
|
+
}
|
|
692
|
+
|
|
618
693
|
/* reduced motion */
|
|
619
694
|
@media (prefers-reduced-motion: reduce) {
|
|
620
695
|
* { animation-duration: .001ms !important; transition-duration: .001ms !important; }
|
|
@@ -1508,10 +1508,13 @@
|
|
|
1508
1508
|
|
|
1509
1509
|
<!-- Cytoscape.js REMOVED in v3.4.1 — replaced by Sigma.js WebGL -->
|
|
1510
1510
|
|
|
1511
|
-
<!-- Sigma.js
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1511
|
+
<!-- Sigma.js + graphology RETIRED in 4.0.6 along with js/knowledge-graph.js.
|
|
1512
|
+
The live knowledge graph (js/od-graph.js) is a canvas-2D renderer with
|
|
1513
|
+
its own force simulation and never constructed a Sigma instance — on a
|
|
1514
|
+
running daemon window.sigmaInstance and window.sigmaGraph were both
|
|
1515
|
+
null while the graph was on screen. The remaining references to Sigma
|
|
1516
|
+
helpers in graph-event-bus.js and memory-chat.js are all behind
|
|
1517
|
+
`typeof … === 'function'` guards and degrade silently. -->
|
|
1515
1518
|
|
|
1516
1519
|
<!-- Modular JS (v2.5 — split from monolith app.js) -->
|
|
1517
1520
|
<script src="static/js/core.js"></script>
|
|
@@ -1524,8 +1527,14 @@
|
|
|
1524
1527
|
|
|
1525
1528
|
<!-- v3.4.1: Event bus (must load before graph + chat) -->
|
|
1526
1529
|
<script src="static/js/graph-event-bus.js"></script>
|
|
1527
|
-
<!--
|
|
1528
|
-
|
|
1530
|
+
<!-- js/knowledge-graph.js RETIRED in 4.0.6. It rendered into #graph-tab,
|
|
1531
|
+
which measured 0x0 with 0 children on a live daemon, and it logged
|
|
1532
|
+
"[Sigma] Container hidden (0 dimensions), deferring render" on every
|
|
1533
|
+
load without ever resuming — then reported "Graph already rendered".
|
|
1534
|
+
The live renderer is js/od-graph.js, which owns #odg-stage.
|
|
1535
|
+
It also defined window.loadGraph; od-graph.js now provides that shim
|
|
1536
|
+
(core.js:552 calls it unguarded on init). filterState/renderGraph are
|
|
1537
|
+
only consumed behind typeof guards in clusters.js and modal.js. -->
|
|
1529
1538
|
<!-- Shared UI utilities (showLoadingSpinner, updateGraphStats, etc.) -->
|
|
1530
1539
|
<script src="static/js/graph-ui.js"></script>
|
|
1531
1540
|
<!-- Filter functions (filterByCluster, filterByEntity — used by clusters.js, modal.js) -->
|
|
@@ -1546,8 +1555,16 @@
|
|
|
1546
1555
|
<script src="static/js/settings.js"></script>
|
|
1547
1556
|
<script src="static/js/events.js"></script>
|
|
1548
1557
|
<script src="static/js/agents.js"></script>
|
|
1549
|
-
<!-- Brain tab
|
|
1550
|
-
|
|
1558
|
+
<!-- Brain tab: rendered by od-brain.js (loaded below).
|
|
1559
|
+
js/brain.js (the v3.4.21 implementation) was RETIRED in 4.0.6. Both files
|
|
1560
|
+
were being loaded and both rendered into the Brain pane — od-brain.js into
|
|
1561
|
+
#brain-pane, brain.js into #brain-content nested inside it — so the pane's
|
|
1562
|
+
copy came from whichever wrote last. od-brain.js already defines
|
|
1563
|
+
window.loadBrain (see its "Legacy compatibility" shim) and, loading second,
|
|
1564
|
+
already won that global; but brain.js also bound its own click handlers to
|
|
1565
|
+
its internal closure, so it kept re-rendering the pane behind od-brain.js.
|
|
1566
|
+
The file is retained on disk for one release in case a rollback is needed. -->
|
|
1567
|
+
|
|
1551
1568
|
<script src="static/js/feedback.js"></script>
|
|
1552
1569
|
<script src="static/js/lifecycle.js"></script>
|
|
1553
1570
|
<script src="static/js/compliance.js"></script>
|
|
@@ -1574,18 +1591,18 @@
|
|
|
1574
1591
|
<script src="static/js/event-delegation.js?v=379"></script>
|
|
1575
1592
|
<!-- OD screen modules (v3.7.9): approved-design panes wired to live data.
|
|
1576
1593
|
Load LAST so their window.load<Screen> overrides win over the legacy loaders. -->
|
|
1577
|
-
<script src="static/js/od-brain.js?v=
|
|
1594
|
+
<script src="static/js/od-brain.js?v=4f233c96"></script>
|
|
1578
1595
|
<script src="static/js/od-components.js?v=382"></script>
|
|
1579
1596
|
<script src="static/js/od-health.js?v=382"></script>
|
|
1580
1597
|
<script src="static/js/od-operations.js?v=380"></script>
|
|
1581
1598
|
<script src="static/js/od-compliance-ext.js?v=100"></script>
|
|
1582
1599
|
<script src="static/js/od-ops-health.js?v=400"></script>
|
|
1583
1600
|
<script src="static/js/od-team.js?v=379"></script>
|
|
1584
|
-
<script src="static/js/od-graph.js?v=
|
|
1601
|
+
<script src="static/js/od-graph.js?v=6812bf6c"></script>
|
|
1585
1602
|
<script src="static/js/od-memories.js?v=379"></script>
|
|
1586
1603
|
<script src="static/js/od-entities.js?v=379"></script>
|
|
1587
1604
|
<!-- Multi-Agent Memory pane (v3.8.0): visualises memory written by multiple agents -->
|
|
1588
|
-
<script src="static/js/od-agents.js?v=
|
|
1605
|
+
<script src="static/js/od-agents.js?v=c75ff3c2"></script>
|
|
1589
1606
|
<script src="static/js/od-skills.js?v=379"></script>
|
|
1590
1607
|
<script src="static/js/od-mesh.js?v=379"></script>
|
|
1591
1608
|
<script src="static/js/od-optimize.js?v=379"></script>
|
|
@@ -158,10 +158,22 @@
|
|
|
158
158
|
'<h3>Recent memories</h3>' +
|
|
159
159
|
'<span class="sub">newest first</span>' +
|
|
160
160
|
'<div class="spacer"></div>' +
|
|
161
|
+
/* max-width + min-width:0 are load-bearing, not cosmetic.
|
|
162
|
+
A <select> sizes itself to its LONGEST OPTION, and the options
|
|
163
|
+
here are agent ids — which for daemon capabilities look like
|
|
164
|
+
"daemon-capability:811f335e55fac7f456aadd68938b862a...", 75+
|
|
165
|
+
characters. Measured on a real store: the control rendered
|
|
166
|
+
1,178px wide inside a 714px pane, dragging the pane's
|
|
167
|
+
scrollWidth to 1,336px. That is what broke this section's
|
|
168
|
+
layout and pushed the table off the right edge.
|
|
169
|
+
min-width:0 is required because this sits in a flex row, where
|
|
170
|
+
the default min-width:auto refuses to shrink below the
|
|
171
|
+
content's intrinsic size and defeats max-width. */
|
|
161
172
|
'<select id="od-agents-filter"' +
|
|
162
173
|
' style="font-size:12.5px;padding:3px 8px;border-radius:var(--r-sm);' +
|
|
163
174
|
'background:var(--card-2);border:1px solid var(--border);' +
|
|
164
|
-
'color:var(--fg-1)
|
|
175
|
+
'color:var(--fg-1);max-width:220px;min-width:0;' +
|
|
176
|
+
'text-overflow:ellipsis">' +
|
|
165
177
|
'<option value="">All agents</option>' +
|
|
166
178
|
'</select>' +
|
|
167
179
|
'</div>' +
|
|
@@ -259,6 +271,30 @@
|
|
|
259
271
|
}
|
|
260
272
|
|
|
261
273
|
// ── Populate agent filter dropdown ────────────────────────────────────────
|
|
274
|
+
/* Turn an agent id into something a person can read in a dropdown.
|
|
275
|
+
Ids arrive in three shapes on a real store:
|
|
276
|
+
"claude", "codex" -> already readable
|
|
277
|
+
"daemon-capability:<64 hex>" -> kind + short fingerprint
|
|
278
|
+
"cli-offline-canonical:local-capability:cli:uid:501:<64 hex>:d63110"
|
|
279
|
+
Showing the raw string is what made the control 1,178px wide, and a
|
|
280
|
+
64-character hash tells the reader nothing anyway. Keep the leading,
|
|
281
|
+
meaningful segments and a short fingerprint so two capabilities of the
|
|
282
|
+
same kind remain distinguishable. The full id stays on opt.value (used
|
|
283
|
+
for filtering) and on opt.title (hover). */
|
|
284
|
+
function shortAgentLabel(id) {
|
|
285
|
+
if (!id) return 'unknown';
|
|
286
|
+
if (id.length <= 28) return id;
|
|
287
|
+
var parts = id.split(':');
|
|
288
|
+
var head = parts[0] || id;
|
|
289
|
+
/* find the first long hex-looking segment and reduce it to 6 chars */
|
|
290
|
+
for (var i = 1; i < parts.length; i++) {
|
|
291
|
+
if (/^[0-9a-f]{16,}$/i.test(parts[i])) {
|
|
292
|
+
return head + ':' + parts[i].slice(0, 6) + '…';
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return head.length <= 28 ? head + ':…' : head.slice(0, 27) + '…';
|
|
296
|
+
}
|
|
297
|
+
|
|
262
298
|
function populateFilter(agents) {
|
|
263
299
|
var sel = document.getElementById('od-agents-filter');
|
|
264
300
|
if (!sel) return;
|
|
@@ -267,8 +303,10 @@
|
|
|
267
303
|
if (!agents) return;
|
|
268
304
|
agents.forEach(function (a) {
|
|
269
305
|
var opt = document.createElement('option');
|
|
270
|
-
|
|
271
|
-
opt.
|
|
306
|
+
var id = String(a.agent_id || '');
|
|
307
|
+
opt.value = id; /* filtering needs the FULL id */
|
|
308
|
+
opt.textContent = shortAgentLabel(id); /* the human reads a short one */
|
|
309
|
+
opt.title = id || 'unknown'; /* full id still available on hover */
|
|
272
310
|
sel.appendChild(opt);
|
|
273
311
|
});
|
|
274
312
|
}
|
|
@@ -293,7 +331,13 @@
|
|
|
293
331
|
var rawSrc = String(r.source_type || '—');
|
|
294
332
|
var rawSess = String(r.session_id || '—');
|
|
295
333
|
|
|
296
|
-
|
|
334
|
+
// Same treatment the content and session columns already get. The agent
|
|
335
|
+
// id is the one field that was rendered in full, and with nowrap that
|
|
336
|
+
// made the Agent column 632px wide for a 75-character
|
|
337
|
+
// "daemon-capability:<64 hex>" — squeezing every other column off the
|
|
338
|
+
// right edge. Full id stays in the title attribute.
|
|
339
|
+
var agId = escapeHtml(shortAgentLabel(rawId));
|
|
340
|
+
var agIdFull = escapeHtml(rawId);
|
|
297
341
|
var srcType = escapeHtml(rawSrc);
|
|
298
342
|
var dt = escapeHtml(fmtDate(r.created_at));
|
|
299
343
|
|
|
@@ -317,7 +361,7 @@
|
|
|
317
361
|
' style="width:22px;height:22px;font-size:10px;background:' + avatarBg + '">' +
|
|
318
362
|
letter +
|
|
319
363
|
'</span>' +
|
|
320
|
-
'<span class="mono" style="font-size:12px">' + agId + '</span>' +
|
|
364
|
+
'<span class="mono" style="font-size:12px" title="' + agIdFull + '">' + agId + '</span>' +
|
|
321
365
|
'</span>' +
|
|
322
366
|
'</td>' +
|
|
323
367
|
'<td style="max-width:340px;font-size:13px;padding:7px 12px 7px 0"' +
|