superlocalmemory 3.8.0 → 3.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +60 -0
- package/README.md +32 -120
- package/package.json +9 -2
- package/plugin/.claude-plugin/plugin.json +1 -2
- package/plugin/CLAUDE.md +3 -3
- package/plugin/agents/slm-governance-advisor.md +1 -1
- package/plugin/agents/slm-loop-runner.md +1 -1
- package/plugin/agents/slm-memory-advisor.md +1 -1
- package/plugin/agents/slm-optimize-advisor.md +1 -1
- package/plugin/requirements.txt +1 -1
- package/plugin/skills/slm-cache/SKILL.md +1 -1
- package/plugin/skills/slm-compress/SKILL.md +1 -1
- package/plugin/skills/slm-governance/SKILL.md +1 -1
- package/plugin/skills/slm-graph/SKILL.md +1 -1
- package/plugin/skills/slm-loop/SKILL.md +1 -1
- package/plugin/skills/slm-mesh/SKILL.md +1 -1
- package/plugin/skills/slm-profile/SKILL.md +1 -1
- package/plugin/skills/slm-recall/SKILL.md +1 -1
- package/plugin/skills/slm-remember/SKILL.md +1 -1
- package/plugin/skills/slm-scope/SKILL.md +1 -1
- package/plugin/skills/slm-session/SKILL.md +1 -1
- package/plugin/skills/slm-status/SKILL.md +1 -1
- package/plugin-src/rules/AGENTS.md +1 -1
- package/plugin-src/skills/slm-cache/SKILL.md +1 -1
- package/plugin-src/skills/slm-compress/SKILL.md +1 -1
- package/plugin-src/skills/slm-graph/SKILL.md +1 -1
- package/plugin-src/skills/slm-recall/SKILL.md +1 -1
- package/plugin-src/skills/slm-remember/SKILL.md +1 -1
- package/plugin-src/skills/slm-session/SKILL.md +1 -1
- package/plugin-src/skills/slm-status/SKILL.md +1 -1
- package/pyproject.toml +2 -1
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/commands.py +134 -7
- package/src/superlocalmemory/cli/daemon.py +7 -0
- package/src/superlocalmemory/cli/loop_cmd.py +2 -7
- package/src/superlocalmemory/cli/main.py +10 -4
- package/src/superlocalmemory/cli/version_banner.py +17 -3
- package/src/superlocalmemory/core/backend_orchestrator.py +18 -16
- package/src/superlocalmemory/core/embedding_worker.py +4 -5
- package/src/superlocalmemory/core/embeddings.py +119 -45
- package/src/superlocalmemory/core/engine.py +24 -21
- package/src/superlocalmemory/core/engine_ingestion.py +332 -45
- package/src/superlocalmemory/core/ingestion_command.py +134 -25
- package/src/superlocalmemory/core/injection.py +12 -7
- package/src/superlocalmemory/core/maintenance_scheduler.py +17 -7
- package/src/superlocalmemory/core/recall_pipeline.py +5 -3
- package/src/superlocalmemory/core/store_pipeline.py +153 -20
- package/src/superlocalmemory/hooks/hook_handlers.py +6 -1
- package/src/superlocalmemory/hooks/portable_kit.py +34 -2
- package/src/superlocalmemory/learning/model_rollback.py +3 -0
- package/src/superlocalmemory/learning/ranker_retrain_online.py +2 -0
- package/src/superlocalmemory/learning/reward.py +50 -0
- package/src/superlocalmemory/learning/source_quality.py +523 -1
- package/src/superlocalmemory/loops/ledger.py +25 -5
- package/src/superlocalmemory/mcp/server.py +11 -30
- package/src/superlocalmemory/mcp/tools_core.py +4 -3
- package/src/superlocalmemory/mcp/tools_learning.py +2 -2
- package/src/superlocalmemory/retrieval/engine.py +53 -21
- package/src/superlocalmemory/retrieval/reranker.py +3 -4
- package/src/superlocalmemory/server/config_file.py +90 -0
- package/src/superlocalmemory/server/origin.py +50 -0
- package/src/superlocalmemory/server/routes/backup.py +293 -70
- package/src/superlocalmemory/server/routes/behavioral.py +336 -59
- package/src/superlocalmemory/server/routes/brain.py +57 -16
- package/src/superlocalmemory/server/routes/config_api.py +84 -82
- package/src/superlocalmemory/server/routes/entity.py +100 -23
- package/src/superlocalmemory/server/routes/evolution.py +103 -100
- package/src/superlocalmemory/server/routes/learning.py +276 -105
- package/src/superlocalmemory/server/routes/learning_telemetry.py +153 -0
- package/src/superlocalmemory/server/routes/mesh.py +121 -32
- package/src/superlocalmemory/server/routes/ratelimit.py +33 -25
- package/src/superlocalmemory/server/routes/stats.py +93 -155
- package/src/superlocalmemory/server/routes/token.py +3 -13
- package/src/superlocalmemory/server/routes/v3_api.py +64 -20
- package/src/superlocalmemory/server/unified_daemon.py +467 -40
- package/src/superlocalmemory/storage/migration_runner.py +79 -1
- package/src/superlocalmemory/storage/migrations/M010_evolution_config.py +5 -0
- package/src/superlocalmemory/storage/migrations/M028_fact_entity_associations.py +270 -0
- package/src/superlocalmemory/storage/migrations/M029_behavioral_history_indexes.py +137 -0
- package/src/superlocalmemory/storage/migrations/M030_entity_explorer_indexes.py +93 -0
- package/src/superlocalmemory/storage/migrations/__init__.py +4 -0
- package/src/superlocalmemory/storage/schema.py +49 -1
- package/src/superlocalmemory/storage/schema_v32.py +2 -0
- package/src/superlocalmemory/storage/schema_v347.py +4 -0
- package/src/superlocalmemory/ui/index.html +3 -6
- package/src/superlocalmemory/ui/js/core.js +52 -9
- package/src/superlocalmemory/ui/js/dashboard.js +169 -82
- package/src/superlocalmemory/ui/js/od-backup.js +156 -65
- package/src/superlocalmemory/ui/js/od-brain.js +88 -51
- package/src/superlocalmemory/ui/js/od-entities.js +22 -22
- package/src/superlocalmemory/ui/js/od-graph.js +11 -4
- package/src/superlocalmemory/ui/js/od-memories.js +47 -5
- package/src/superlocalmemory/ui/js/od-mesh.js +23 -9
- package/src/superlocalmemory/ui/js/od-settings.js +113 -59
- package/src/superlocalmemory/ui/js/od-shell.js +249 -33
- package/src/superlocalmemory/ui/js/od-skills.js +44 -17
- package/src/superlocalmemory/ui/js/settings.js +15 -1
- package/plugin-src/.mcp.json +0 -12
- package/plugin-src/agents/slm-governance-advisor.md +0 -80
- package/plugin-src/agents/slm-loop-runner.md +0 -71
- package/plugin-src/agents/slm-memory-advisor.md +0 -49
- package/plugin-src/agents/slm-optimize-advisor.md +0 -44
- package/plugin-src/commands/slm-loop.md +0 -31
- package/plugin-src/hooks/.gitkeep +0 -0
- package/plugin-src/hooks/hooks.json +0 -102
- package/plugin-src/manifest.json +0 -30
- package/plugin-src/requirements.txt +0 -1
- package/plugin-src/rules/CLAUDE.md.fragment +0 -44
- package/plugin-src/scripts/ensure-venv.bat +0 -122
- package/plugin-src/scripts/ensure-venv.sh +0 -105
- package/plugin-src/scripts/slm-launch +0 -62
- package/plugin-src/scripts/slm-launch.bat +0 -23
- package/plugin-src/settings.json +0 -25
- package/plugin-src/skills/slm-governance/SKILL.md +0 -248
- package/plugin-src/skills/slm-loop/SKILL.md +0 -99
- package/plugin-src/skills/slm-mesh/SKILL.md +0 -282
- package/plugin-src/skills/slm-profile/SKILL.md +0 -148
- package/plugin-src/skills/slm-scope/SKILL.md +0 -176
|
@@ -38,6 +38,8 @@ _TABLES: Final[tuple[str, ...]] = (
|
|
|
38
38
|
"memories",
|
|
39
39
|
"atomic_facts",
|
|
40
40
|
"canonical_entities",
|
|
41
|
+
"fact_entity_associations",
|
|
42
|
+
"fact_entity_association_repair_state",
|
|
41
43
|
"entity_aliases",
|
|
42
44
|
"entity_profiles",
|
|
43
45
|
"memory_scenes",
|
|
@@ -328,7 +330,52 @@ CREATE INDEX IF NOT EXISTS idx_entities_profile
|
|
|
328
330
|
CREATE INDEX IF NOT EXISTS idx_entities_name_lower
|
|
329
331
|
ON canonical_entities (profile_id, canonical_name COLLATE NOCASE);
|
|
330
332
|
CREATE INDEX IF NOT EXISTS idx_entities_type
|
|
331
|
-
ON canonical_entities (profile_id, entity_type);
|
|
333
|
+
ON canonical_entities (profile_id, entity_type);
|
|
334
|
+
CREATE INDEX IF NOT EXISTS idx_entities_profile_fact_count
|
|
335
|
+
ON canonical_entities (profile_id, fact_count DESC);
|
|
336
|
+
CREATE INDEX IF NOT EXISTS idx_entities_profile_type_fact_count
|
|
337
|
+
ON canonical_entities (
|
|
338
|
+
profile_id, entity_type COLLATE NOCASE, fact_count DESC
|
|
339
|
+
);"""
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
# ---------------------------------------------------------------------------
|
|
343
|
+
# Normalized fact/entity associations (also the ingestion effect ledger)
|
|
344
|
+
# ---------------------------------------------------------------------------
|
|
345
|
+
|
|
346
|
+
_SQL_FACT_ENTITY_ASSOCIATIONS: Final[str] = """
|
|
347
|
+
CREATE TABLE IF NOT EXISTS fact_entity_associations (
|
|
348
|
+
profile_id TEXT NOT NULL,
|
|
349
|
+
fact_id TEXT NOT NULL,
|
|
350
|
+
entity_id TEXT NOT NULL,
|
|
351
|
+
first_operation_id TEXT NOT NULL DEFAULT '',
|
|
352
|
+
count_applied INTEGER NOT NULL DEFAULT 0
|
|
353
|
+
CHECK (count_applied IN (0, 1)),
|
|
354
|
+
created_at TEXT NOT NULL DEFAULT (
|
|
355
|
+
strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
356
|
+
),
|
|
357
|
+
PRIMARY KEY (profile_id, fact_id, entity_id),
|
|
358
|
+
FOREIGN KEY (fact_id) REFERENCES atomic_facts(fact_id) ON DELETE CASCADE,
|
|
359
|
+
FOREIGN KEY (entity_id)
|
|
360
|
+
REFERENCES canonical_entities(entity_id) ON DELETE CASCADE
|
|
361
|
+
);
|
|
362
|
+
CREATE INDEX IF NOT EXISTS idx_fact_entity_associations_entity
|
|
363
|
+
ON fact_entity_associations(profile_id, entity_id, fact_id);
|
|
364
|
+
CREATE TABLE IF NOT EXISTS fact_entity_association_repair_state (
|
|
365
|
+
repair_key TEXT PRIMARY KEY,
|
|
366
|
+
state TEXT NOT NULL DEFAULT 'pending'
|
|
367
|
+
CHECK (state IN ('pending', 'running', 'retrying', 'complete')),
|
|
368
|
+
target_fact_rowid INTEGER NOT NULL DEFAULT -1,
|
|
369
|
+
last_fact_rowid INTEGER NOT NULL DEFAULT 0,
|
|
370
|
+
scanned INTEGER NOT NULL DEFAULT 0,
|
|
371
|
+
inserted INTEGER NOT NULL DEFAULT 0,
|
|
372
|
+
last_error TEXT NOT NULL DEFAULT '',
|
|
373
|
+
updated_at TEXT NOT NULL
|
|
374
|
+
);
|
|
375
|
+
INSERT OR IGNORE INTO fact_entity_association_repair_state
|
|
376
|
+
(repair_key, state, target_fact_rowid, updated_at)
|
|
377
|
+
VALUES ('historical-backfill', 'pending', -1, '');
|
|
378
|
+
"""
|
|
332
379
|
|
|
333
380
|
|
|
334
381
|
# ---------------------------------------------------------------------------
|
|
@@ -752,6 +799,7 @@ _DDL_ORDERED: Final[tuple[str, ...]] = (
|
|
|
752
799
|
_SQL_MEMORIES,
|
|
753
800
|
_SQL_ATOMIC_FACTS,
|
|
754
801
|
_SQL_CANONICAL_ENTITIES,
|
|
802
|
+
_SQL_FACT_ENTITY_ASSOCIATIONS,
|
|
755
803
|
_SQL_ENTITY_ALIASES,
|
|
756
804
|
_SQL_ENTITY_PROFILES,
|
|
757
805
|
_SQL_MEMORY_SCENES,
|
|
@@ -359,6 +359,8 @@ V32_DDL: list[str] = [
|
|
|
359
359
|
ON soft_prompt_templates(profile_id, active);
|
|
360
360
|
CREATE INDEX IF NOT EXISTS idx_soft_prompt_category
|
|
361
361
|
ON soft_prompt_templates(profile_id, category);
|
|
362
|
+
CREATE INDEX IF NOT EXISTS idx_soft_prompts_profile_active_category
|
|
363
|
+
ON soft_prompt_templates(profile_id, active, category, prompt_id);
|
|
362
364
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_soft_prompt_unique_cat
|
|
363
365
|
ON soft_prompt_templates(profile_id, category)
|
|
364
366
|
WHERE active = 1;
|
|
@@ -48,6 +48,8 @@ CREATE INDEX IF NOT EXISTS idx_tool_events_session
|
|
|
48
48
|
ON tool_events(session_id);
|
|
49
49
|
CREATE INDEX IF NOT EXISTS idx_tool_events_project
|
|
50
50
|
ON tool_events(project_path);
|
|
51
|
+
CREATE INDEX IF NOT EXISTS idx_tool_events_profile_created
|
|
52
|
+
ON tool_events(profile_id, created_at DESC);
|
|
51
53
|
"""
|
|
52
54
|
|
|
53
55
|
# ---------------------------------------------------------------------------
|
|
@@ -80,6 +82,8 @@ CREATE INDEX IF NOT EXISTS idx_assertions_project
|
|
|
80
82
|
ON behavioral_assertions(project_path, profile_id);
|
|
81
83
|
CREATE INDEX IF NOT EXISTS idx_assertions_category
|
|
82
84
|
ON behavioral_assertions(category);
|
|
85
|
+
CREATE INDEX IF NOT EXISTS idx_assertions_profile_confidence
|
|
86
|
+
ON behavioral_assertions(profile_id, confidence DESC);
|
|
83
87
|
"""
|
|
84
88
|
|
|
85
89
|
# ---------------------------------------------------------------------------
|
|
@@ -182,7 +182,6 @@
|
|
|
182
182
|
<div class="value num" id="k-clu">—</div>
|
|
183
183
|
<div class="delta" id="k-clu-delta"> </div>
|
|
184
184
|
<div class="spark" id="sp-clu"></div>
|
|
185
|
-
<!-- TODO: endpoint /api/v3/clusters/count does not exist yet -->
|
|
186
185
|
</div>
|
|
187
186
|
</section>
|
|
188
187
|
|
|
@@ -192,8 +191,8 @@
|
|
|
192
191
|
<!-- memory growth + quick actions -->
|
|
193
192
|
<div class="card">
|
|
194
193
|
<div class="card-head">
|
|
195
|
-
<h3>Memory
|
|
196
|
-
<span class="sub">facts
|
|
194
|
+
<h3>Memory activity</h3>
|
|
195
|
+
<span class="sub">facts added by day · past year</span>
|
|
197
196
|
<div class="spacer"></div>
|
|
198
197
|
</div>
|
|
199
198
|
<div class="card-pad">
|
|
@@ -300,9 +299,7 @@
|
|
|
300
299
|
<div class="card">
|
|
301
300
|
<div class="card-head"><h3>Where memories come from</h3></div>
|
|
302
301
|
<div class="card-pad">
|
|
303
|
-
<!-- populated by loadDashboardSources()
|
|
304
|
-
<!-- TODO: endpoint /api/v3/ingestion/sources does not exist yet;
|
|
305
|
-
showing static breakdown from /api/stats for now -->
|
|
302
|
+
<!-- populated by loadDashboardSources() from profile-scoped provenance -->
|
|
306
303
|
<div id="src">
|
|
307
304
|
<div class="muted" style="padding:16px;text-align:center;font-size:13px">Loading...</div>
|
|
308
305
|
</div>
|
|
@@ -35,11 +35,11 @@ window.slmInstallToken = (function () {
|
|
|
35
35
|
};
|
|
36
36
|
})();
|
|
37
37
|
|
|
38
|
-
// Global fetch patch: apply the abort timeout to every
|
|
38
|
+
// Global fetch patch: apply the abort timeout to every same-origin request
|
|
39
39
|
// automatically. 17 UI modules call bare fetch() — patching here avoids
|
|
40
40
|
// touching each one and guarantees no future callsite can regress to an
|
|
41
|
-
// un-timed fetch that holds the spinner forever.
|
|
42
|
-
//
|
|
41
|
+
// un-timed fetch that holds the spinner forever. External URLs are passed
|
|
42
|
+
// through unchanged. Callers that already supply
|
|
43
43
|
// `signal` keep their own behavior. `init.timeoutMs` lets callers override
|
|
44
44
|
// the default per-request.
|
|
45
45
|
(function patchFetch() {
|
|
@@ -49,25 +49,68 @@ window.slmInstallToken = (function () {
|
|
|
49
49
|
window.__slmOriginalFetch = _origFetch;
|
|
50
50
|
window.fetch = function (input, init) {
|
|
51
51
|
init = Object.assign({}, init || {});
|
|
52
|
-
var urlStr = typeof input === 'string'
|
|
53
|
-
|
|
52
|
+
var urlStr = typeof input === 'string'
|
|
53
|
+
? input
|
|
54
|
+
: (input && (input.url || input.href)) || '';
|
|
55
|
+
// Resolve every URL before deciding whether it is local. In particular,
|
|
56
|
+
// protocol-relative URLs such as //host/path inherit the current scheme
|
|
57
|
+
// but are not same-origin and must never receive the install token.
|
|
58
|
+
var isSameOrigin = false;
|
|
59
|
+
var requestUrl = null;
|
|
60
|
+
try {
|
|
61
|
+
requestUrl = new URL(urlStr, window.location.href);
|
|
62
|
+
isSameOrigin = requestUrl.origin === window.location.origin;
|
|
63
|
+
} catch (e) {
|
|
64
|
+
// A malformed URL is handled by native fetch. Treat it as external
|
|
65
|
+
// here so it cannot obtain local credentials while failing.
|
|
66
|
+
}
|
|
54
67
|
var method = String(
|
|
55
68
|
init.method || (input && input.method) || 'GET'
|
|
56
69
|
).toUpperCase();
|
|
57
70
|
var mutating = ['POST', 'PUT', 'PATCH', 'DELETE'].indexOf(method) !== -1;
|
|
71
|
+
var invalidatesCache = init.slmInvalidatesCache !== false;
|
|
72
|
+
var requiresWriteAuth = init.slmRequiresWriteAuth !== false;
|
|
73
|
+
delete init.slmInvalidatesCache;
|
|
74
|
+
delete init.slmRequiresWriteAuth;
|
|
75
|
+
|
|
76
|
+
function invalidateAfterMutation(request) {
|
|
77
|
+
if (!isSameOrigin || !mutating || !invalidatesCache) return request;
|
|
78
|
+
return request.then(function (response) {
|
|
79
|
+
if (
|
|
80
|
+
response && response.ok &&
|
|
81
|
+
typeof window.slmInvalidatePanes === 'function'
|
|
82
|
+
) {
|
|
83
|
+
window.slmInvalidatePanes();
|
|
84
|
+
}
|
|
85
|
+
if (
|
|
86
|
+
response && response.ok &&
|
|
87
|
+
typeof window.slmInvalidateDashboardCache === 'function'
|
|
88
|
+
) {
|
|
89
|
+
window.slmInvalidateDashboardCache();
|
|
90
|
+
}
|
|
91
|
+
return response;
|
|
92
|
+
});
|
|
93
|
+
}
|
|
58
94
|
|
|
59
95
|
function send() {
|
|
60
|
-
if (!
|
|
61
|
-
return _origFetch(input, init);
|
|
96
|
+
if (!isSameOrigin || init.signal) {
|
|
97
|
+
return invalidateAfterMutation(_origFetch(input, init));
|
|
62
98
|
}
|
|
63
99
|
var controller = new AbortController();
|
|
64
100
|
var timeoutMs = init.timeoutMs || window.SLM_FETCH_TIMEOUT_MS;
|
|
65
101
|
var timer = setTimeout(function () { controller.abort(); }, timeoutMs);
|
|
66
102
|
init.signal = controller.signal;
|
|
67
|
-
return
|
|
103
|
+
return invalidateAfterMutation(
|
|
104
|
+
_origFetch(input, init).finally(function () { clearTimeout(timer); })
|
|
105
|
+
);
|
|
68
106
|
}
|
|
69
107
|
|
|
70
|
-
if (
|
|
108
|
+
if (
|
|
109
|
+
isSameOrigin &&
|
|
110
|
+
mutating &&
|
|
111
|
+
requiresWriteAuth &&
|
|
112
|
+
(!requestUrl || requestUrl.pathname !== '/internal/token')
|
|
113
|
+
) {
|
|
71
114
|
return window.slmInstallToken(false).then(function (token) {
|
|
72
115
|
if (!token) throw new Error('local write credential unavailable');
|
|
73
116
|
var headers = new Headers(
|
|
@@ -5,29 +5,64 @@
|
|
|
5
5
|
// loadDashboard() → GET /api/v3/dashboard → system card (#dashboard-mode etc.)
|
|
6
6
|
// loadDashboardStats() → GET /api/stats → OD KPI strip (#k-mem, #k-nodes, #k-edges)
|
|
7
7
|
// loadDashboardFeed() → GET /api/memories?limit=6 → activity feed (#feed)
|
|
8
|
-
// loadDashboardSources() →
|
|
9
|
-
//
|
|
8
|
+
// loadDashboardSources() → real provenance from /api/stats → capture-sources (#src)
|
|
9
|
+
// loadDashboardTimeline() → real daily fact counts from /api/timeline → #sp-big
|
|
10
|
+
|
|
11
|
+
var DASHBOARD_CACHE_MS = 30000;
|
|
12
|
+
var dashboardLastRefresh = 0;
|
|
13
|
+
var dashboardRefreshInFlight = null;
|
|
14
|
+
var dashboardCacheGeneration = 0;
|
|
15
|
+
var dashboardRefreshQueued = false;
|
|
16
|
+
|
|
17
|
+
window.slmInvalidateDashboardCache = function() {
|
|
18
|
+
dashboardCacheGeneration += 1;
|
|
19
|
+
dashboardLastRefresh = 0;
|
|
20
|
+
if (dashboardRefreshInFlight) dashboardRefreshQueued = true;
|
|
21
|
+
};
|
|
10
22
|
|
|
11
23
|
// Auto-refresh dashboard when tab becomes visible (fixes stale data after settings change)
|
|
12
24
|
document.addEventListener('visibilitychange', function() {
|
|
13
25
|
if (!document.hidden) refreshDashboard();
|
|
14
26
|
});
|
|
15
27
|
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
window.addEventListener('focus', function() { loadDashboard(); });
|
|
28
|
+
// Focus and tab navigation use a short cache window. This keeps normal
|
|
29
|
+
// browser switching from repeatedly querying a large local database.
|
|
30
|
+
window.addEventListener('focus', function() { refreshDashboard(); });
|
|
20
31
|
|
|
21
32
|
// Load when the Dashboard tab is shown via the sidebar. od-shell.js's activateTab()
|
|
22
33
|
// dispatches shown.bs.tab on the hidden #dashboard-tab button.
|
|
23
34
|
document.getElementById('dashboard-tab')?.addEventListener('shown.bs.tab', function() { refreshDashboard(); });
|
|
24
35
|
|
|
25
|
-
// refreshDashboard
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
36
|
+
// refreshDashboard coalesces concurrent startup/navigation events and keeps a
|
|
37
|
+
// 30-second snapshot. Mutations pass {force:true} to re-read daemon truth.
|
|
38
|
+
window.refreshDashboard = function(options) {
|
|
39
|
+
var force = options && options.force === true;
|
|
40
|
+
var now = Date.now();
|
|
41
|
+
if (dashboardRefreshInFlight) {
|
|
42
|
+
if (force) dashboardRefreshQueued = true;
|
|
43
|
+
return dashboardRefreshInFlight;
|
|
44
|
+
}
|
|
45
|
+
if (!force && dashboardLastRefresh && now - dashboardLastRefresh < DASHBOARD_CACHE_MS) {
|
|
46
|
+
return Promise.resolve();
|
|
47
|
+
}
|
|
48
|
+
var refreshGeneration = dashboardCacheGeneration;
|
|
49
|
+
dashboardRefreshInFlight = Promise.all([
|
|
50
|
+
loadDashboard(),
|
|
51
|
+
loadDashboardStats(),
|
|
52
|
+
loadDashboardTimeline(),
|
|
53
|
+
loadDashboardFeed()
|
|
54
|
+
]).then(function(results) {
|
|
55
|
+
dashboardLastRefresh = refreshGeneration === dashboardCacheGeneration && results.every(Boolean)
|
|
56
|
+
? Date.now()
|
|
57
|
+
: 0;
|
|
58
|
+
}).finally(function() {
|
|
59
|
+
dashboardRefreshInFlight = null;
|
|
60
|
+
}).then(function() {
|
|
61
|
+
if (!dashboardRefreshQueued) return;
|
|
62
|
+
dashboardRefreshQueued = false;
|
|
63
|
+
return window.refreshDashboard({ force: true });
|
|
64
|
+
});
|
|
65
|
+
return dashboardRefreshInFlight;
|
|
31
66
|
};
|
|
32
67
|
|
|
33
68
|
// ----------------------------------------------------------------
|
|
@@ -36,7 +71,7 @@ window.refreshDashboard = function() {
|
|
|
36
71
|
async function loadDashboardStats() {
|
|
37
72
|
try {
|
|
38
73
|
var r = await fetch('/api/stats', { credentials: 'same-origin' });
|
|
39
|
-
if (!r.ok) return;
|
|
74
|
+
if (!r.ok) return false;
|
|
40
75
|
var data = await r.json();
|
|
41
76
|
var ov = data.overview || {};
|
|
42
77
|
|
|
@@ -44,12 +79,14 @@ async function loadDashboardStats() {
|
|
|
44
79
|
var facts = ov.total_facts || 0;
|
|
45
80
|
var nodes = ov.graph_nodes || 0;
|
|
46
81
|
var edges = ov.graph_edges || 0;
|
|
82
|
+
var clusters = ov.total_clusters || 0;
|
|
47
83
|
|
|
48
84
|
// OD KPI strip
|
|
49
85
|
setKpiValue('k-mem', memories);
|
|
50
86
|
setKpiValue('k-nodes', nodes);
|
|
51
87
|
setKpiValue('k-edges', edges);
|
|
52
88
|
setKpiValue('k-facts', facts);
|
|
89
|
+
setKpiValue('k-clu', clusters);
|
|
53
90
|
|
|
54
91
|
// Legacy hidden compat IDs (core.js loadStats() may race — that's OK,
|
|
55
92
|
// the hidden IDs are only a data bridge, not visible).
|
|
@@ -57,51 +94,62 @@ async function loadDashboardStats() {
|
|
|
57
94
|
setTextById('stat-facts', facts);
|
|
58
95
|
setTextById('stat-nodes', nodes);
|
|
59
96
|
setTextById('stat-edges', edges);
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
// (no time-series endpoint exists yet; honest placeholder).
|
|
63
|
-
// TODO: replace with GET /api/v3/stats/history when that endpoint ships.
|
|
64
|
-
if (typeof window.slmSpark === 'function') {
|
|
65
|
-
loadDashboardSparklines(memories, nodes, edges);
|
|
66
|
-
}
|
|
97
|
+
loadDashboardSources(data);
|
|
98
|
+
return true;
|
|
67
99
|
} catch (e) {
|
|
68
100
|
// Non-fatal: KPI strip shows — values
|
|
101
|
+
return false;
|
|
69
102
|
}
|
|
70
103
|
}
|
|
71
104
|
|
|
72
105
|
// ----------------------------------------------------------------
|
|
73
|
-
//
|
|
74
|
-
// No time-series endpoint yet: we synthesise a plausible trailing
|
|
75
|
-
// series anchored to the real current total. Honest placeholder.
|
|
106
|
+
// loadDashboardTimeline — renders real daily fact counts from /api/timeline
|
|
76
107
|
// ----------------------------------------------------------------
|
|
77
|
-
function
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
var
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
108
|
+
async function loadDashboardTimeline() {
|
|
109
|
+
var spBig = document.getElementById('sp-big');
|
|
110
|
+
if (!spBig) return true;
|
|
111
|
+
try {
|
|
112
|
+
var response = await fetch('/api/timeline?days=365&group_by=day&include_categories=false', {
|
|
113
|
+
credentials: 'same-origin'
|
|
114
|
+
});
|
|
115
|
+
if (!response.ok) throw new Error('timeline unavailable');
|
|
116
|
+
var stats = await response.json();
|
|
117
|
+
var points = Array.isArray(stats.timeline) ? stats.timeline : [];
|
|
118
|
+
var byDate = {};
|
|
119
|
+
points.forEach(function(point) {
|
|
120
|
+
if (point && point.period) byDate[String(point.period)] = Number(point.count) || 0;
|
|
121
|
+
});
|
|
122
|
+
var counts = [];
|
|
123
|
+
var today = new Date();
|
|
124
|
+
today.setHours(0, 0, 0, 0);
|
|
125
|
+
for (var dayOffset = 364; dayOffset >= 0; dayOffset--) {
|
|
126
|
+
var day = new Date(today);
|
|
127
|
+
day.setDate(today.getDate() - dayOffset);
|
|
128
|
+
counts.push(byDate[day.toISOString().slice(0, 10)] || 0);
|
|
85
129
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
130
|
+
var total = counts.reduce(function(sum, value) { return sum + value; }, 0);
|
|
131
|
+
var avgEl = document.getElementById('k-avg-day');
|
|
132
|
+
if (avgEl) avgEl.textContent = (total / 365).toFixed(1);
|
|
133
|
+
|
|
134
|
+
if (!total || typeof window.slmSpark !== 'function') {
|
|
135
|
+
spBig.textContent = 'No dated memory history recorded for this profile.';
|
|
136
|
+
spBig.className = 'muted';
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
spBig.className = '';
|
|
140
|
+
spBig.innerHTML = window.slmSpark(counts, {
|
|
141
|
+
w: 600,
|
|
142
|
+
h: 140,
|
|
143
|
+
color: 'var(--violet)'
|
|
144
|
+
});
|
|
145
|
+
if (spBig.querySelector('svg')) {
|
|
146
|
+
spBig.querySelector('svg').style.cssText = 'width:100%;height:140px;display:block;';
|
|
147
|
+
}
|
|
148
|
+
return true;
|
|
149
|
+
} catch (e) {
|
|
150
|
+
spBig.textContent = 'Memory history unavailable.';
|
|
151
|
+
spBig.className = 'muted';
|
|
152
|
+
return false;
|
|
105
153
|
}
|
|
106
154
|
}
|
|
107
155
|
|
|
@@ -110,7 +158,7 @@ function loadDashboardSparklines(memories, nodes, edges) {
|
|
|
110
158
|
// ----------------------------------------------------------------
|
|
111
159
|
async function loadDashboardFeed() {
|
|
112
160
|
var feedEl = document.getElementById('feed');
|
|
113
|
-
if (!feedEl) return;
|
|
161
|
+
if (!feedEl) return true;
|
|
114
162
|
|
|
115
163
|
var CATEGORY_COLORS = {
|
|
116
164
|
'episodic': ['rgba(34,197,94,0.12)', '#22c55e'],
|
|
@@ -131,13 +179,13 @@ async function loadDashboardFeed() {
|
|
|
131
179
|
var r = await fetch('/api/memories?limit=6', { credentials: 'same-origin' });
|
|
132
180
|
if (!r.ok) {
|
|
133
181
|
feedEl.innerHTML = '<div class="muted" style="padding:12px;text-align:center;font-size:13px">No recent activity</div>';
|
|
134
|
-
return;
|
|
182
|
+
return false;
|
|
135
183
|
}
|
|
136
184
|
var data = await r.json();
|
|
137
185
|
var items = data.memories || data.results || data || [];
|
|
138
186
|
if (!Array.isArray(items) || items.length === 0) {
|
|
139
187
|
feedEl.innerHTML = '<div class="muted" style="padding:12px;text-align:center;font-size:13px">No memories yet — start capturing!</div>';
|
|
140
|
-
return;
|
|
188
|
+
return true;
|
|
141
189
|
}
|
|
142
190
|
|
|
143
191
|
// Build feed using DOM methods (XSS-safe — no innerHTML with user content)
|
|
@@ -180,37 +228,76 @@ async function loadDashboardFeed() {
|
|
|
180
228
|
item.appendChild(time);
|
|
181
229
|
feedEl.appendChild(item);
|
|
182
230
|
});
|
|
231
|
+
return true;
|
|
183
232
|
} catch (e) {
|
|
184
233
|
feedEl.innerHTML = '<div class="muted" style="padding:12px;text-align:center;font-size:13px">Activity unavailable</div>';
|
|
234
|
+
return false;
|
|
185
235
|
}
|
|
186
236
|
}
|
|
187
237
|
|
|
188
238
|
// ----------------------------------------------------------------
|
|
189
|
-
// loadDashboardSources —
|
|
190
|
-
// TODO: GET /api/v3/ingestion/sources does not exist yet.
|
|
239
|
+
// loadDashboardSources — profile-scoped provenance returned by /api/stats
|
|
191
240
|
// ----------------------------------------------------------------
|
|
192
|
-
function loadDashboardSources() {
|
|
241
|
+
function loadDashboardSources(stats) {
|
|
193
242
|
var srcEl = document.getElementById('src');
|
|
194
243
|
if (!srcEl) return;
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
'
|
|
213
|
-
|
|
244
|
+
var sourceStatus = stats && stats.ingestion_sources_status;
|
|
245
|
+
if (sourceStatus && sourceStatus.available === false) {
|
|
246
|
+
srcEl.textContent = 'Provenance metrics are temporarily unavailable.';
|
|
247
|
+
srcEl.className = 'muted';
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
var sources = stats && Array.isArray(stats.ingestion_sources)
|
|
251
|
+
? stats.ingestion_sources
|
|
252
|
+
: [];
|
|
253
|
+
if (!sources.length) {
|
|
254
|
+
srcEl.textContent = 'No provenance recorded for this profile.';
|
|
255
|
+
srcEl.className = 'muted';
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
var LABELS = {
|
|
260
|
+
'store': 'Historical / engine store',
|
|
261
|
+
'http': 'Dashboard, hooks, and API',
|
|
262
|
+
'http-observe': 'Auto-capture observations',
|
|
263
|
+
'mcp-offline-worker': 'Queued MCP ingestion',
|
|
264
|
+
'cli-sync': 'CLI sync',
|
|
265
|
+
'unknown': 'Unclassified'
|
|
266
|
+
};
|
|
267
|
+
var COLORS = ['var(--violet)', 'var(--cyan)', 'var(--warn,#f59e0b)', 'var(--fg-3)'];
|
|
268
|
+
var total = sources.reduce(function(sum, source) {
|
|
269
|
+
return sum + (Number(source.count) || 0);
|
|
270
|
+
}, 0);
|
|
271
|
+
|
|
272
|
+
srcEl.className = '';
|
|
273
|
+
srcEl.textContent = '';
|
|
274
|
+
sources.forEach(function(source, index) {
|
|
275
|
+
var count = Number(source.count) || 0;
|
|
276
|
+
var percent = total ? Math.round((count / total) * 100) : 0;
|
|
277
|
+
var row = document.createElement('div');
|
|
278
|
+
row.style.marginBottom = '14px';
|
|
279
|
+
|
|
280
|
+
var heading = document.createElement('div');
|
|
281
|
+
heading.style.cssText = 'display:flex;justify-content:space-between;font-size:13px;margin-bottom:6px';
|
|
282
|
+
var label = document.createElement('span');
|
|
283
|
+
label.textContent = LABELS[source.source_type] || source.source_type || 'Unclassified';
|
|
284
|
+
var value = document.createElement('b');
|
|
285
|
+
value.className = 'num';
|
|
286
|
+
value.textContent = count.toLocaleString() + ' · ' + percent + '%';
|
|
287
|
+
heading.appendChild(label);
|
|
288
|
+
heading.appendChild(value);
|
|
289
|
+
|
|
290
|
+
var meter = document.createElement('div');
|
|
291
|
+
meter.className = 'meter';
|
|
292
|
+
var fill = document.createElement('i');
|
|
293
|
+
fill.style.width = percent + '%';
|
|
294
|
+
fill.style.background = COLORS[index % COLORS.length];
|
|
295
|
+
meter.appendChild(fill);
|
|
296
|
+
|
|
297
|
+
row.appendChild(heading);
|
|
298
|
+
row.appendChild(meter);
|
|
299
|
+
srcEl.appendChild(row);
|
|
300
|
+
});
|
|
214
301
|
}
|
|
215
302
|
|
|
216
303
|
// ----------------------------------------------------------------
|
|
@@ -247,7 +334,7 @@ async function loadDashboard() {
|
|
|
247
334
|
var response = await fetch('/api/v3/dashboard', { credentials: 'same-origin' });
|
|
248
335
|
if (!response.ok) {
|
|
249
336
|
showPaneError(PANE_ID, paneErrorMessage(response.status), loadDashboard, true);
|
|
250
|
-
return;
|
|
337
|
+
return false;
|
|
251
338
|
}
|
|
252
339
|
var data = await response.json();
|
|
253
340
|
|
|
@@ -282,9 +369,11 @@ async function loadDashboard() {
|
|
|
282
369
|
document.querySelectorAll('.mode-btn').forEach(function(btn) {
|
|
283
370
|
btn.classList.toggle('active', btn.dataset.mode === data.mode);
|
|
284
371
|
});
|
|
372
|
+
return true;
|
|
285
373
|
} catch (e) {
|
|
286
374
|
showPaneError(PANE_ID, paneErrorMessage(0), loadDashboard, true);
|
|
287
375
|
console.log('Dashboard load error:', e);
|
|
376
|
+
return false;
|
|
288
377
|
}
|
|
289
378
|
}
|
|
290
379
|
|
|
@@ -293,10 +382,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|
|
293
382
|
// od-shell.js calls activateTab('dashboard-pane') which dispatches shown.bs.tab,
|
|
294
383
|
// but the listener may not yet be attached at that point. Call directly as well.
|
|
295
384
|
setTimeout(function() {
|
|
296
|
-
|
|
297
|
-
loadDashboardStats();
|
|
298
|
-
loadDashboardFeed();
|
|
299
|
-
loadDashboardSources();
|
|
385
|
+
refreshDashboard({ force: true });
|
|
300
386
|
}, 200);
|
|
301
387
|
});
|
|
302
388
|
|
|
@@ -351,8 +437,7 @@ document.getElementById('quick-store-btn')?.addEventListener('click', async func
|
|
|
351
437
|
}
|
|
352
438
|
await r.json();
|
|
353
439
|
if (input) input.value = '';
|
|
354
|
-
|
|
355
|
-
loadDashboardFeed();
|
|
440
|
+
refreshDashboard({ force: true });
|
|
356
441
|
showToast('Stored!');
|
|
357
442
|
} catch(e) {
|
|
358
443
|
showToast('Store failed: ' + e.message);
|
|
@@ -372,6 +457,8 @@ document.getElementById('quick-recall-btn')?.addEventListener('click', function(
|
|
|
372
457
|
fetch('/api/search', {
|
|
373
458
|
method: 'POST', credentials: 'same-origin',
|
|
374
459
|
headers: {'Content-Type': 'application/json'},
|
|
460
|
+
slmInvalidatesCache: false,
|
|
461
|
+
slmRequiresWriteAuth: false,
|
|
375
462
|
body: JSON.stringify({query: query, limit: 5})
|
|
376
463
|
}).then(function(r) {
|
|
377
464
|
btn.disabled = false;
|