Phase 10: Memory Core — auto-extraction knowledge graph

- reactor.py v9.0.0+: memory_extract + memory_store handlers (21 handlers)
  - handle_memory_extract: Haiku-powered fact extraction from conversations
  - handle_memory_store: explicit memory insertion from voice commands
  - FastAPI: /memory/facts CRUD, /memory/context (relevance retrieval), /memory/stats
- chat.php: Tier 0.9k voice commands (remember/forget/recall/memory status)
  - Memory context injected into Groq + Claude system prompts
  - Auto-trigger memory_extract after every LLM response (async, non-blocking)
- memory.php: new API endpoint proxying Arc Reactor memory routes
- api.php: added memory route
- admin/index.php: MEMORY CORE nav + tab (browse by category, search, add/delete facts)
- index.html: MEMORY count in bottom bar (polls every 60s)
This commit is contained in:
2026-06-11 12:33:05 +00:00
parent 93d7594c4f
commit 27a0259e64
5 changed files with 458 additions and 2 deletions
+127 -2
View File
@@ -1081,6 +1081,52 @@ if (!$reply && preg_match('/\b(news|headlines|latest|what.?s happening|current e
// ── Tier 0.9: Arc Protocols — research, triage, remote_exec, screenshot, sysinfo ─ // ── Tier 0.9: Arc Protocols — research, triage, remote_exec, screenshot, sysinfo ─
$arcJobId = null; $arcJobId = null;
// ── Memory Core helpers ───────────────────────────────────────────────────────
function getMemoryContext(string $message, int $limit = 12): string {
try {
$ch = curl_init('http://127.0.0.1:7474/memory/context?limit=' . $limit .
'&message=' . urlencode(substr($message, 0, 200)));
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 2,
CURLOPT_CONNECTTIMEOUT => 1,
]);
$raw = curl_exec($ch);
curl_close($ch);
if (!$raw) return '';
$data = json_decode($raw, true);
return $data['context'] ?? '';
} catch (Throwable $e) {
return '';
}
}
function memoryExtractAsync(string $userMsg, string $assistantMsg, string $sessionId): void {
// Fire-and-forget background memory extraction — does not block the response
$body = json_encode([
'type' => 'memory_extract',
'payload' => [
'user_message' => substr($userMsg, 0, 600),
'assistant_message' => substr($assistantMsg, 0, 600),
'conversation_id' => null,
],
'priority' => 2,
'created_by' => 'chat:' . $sessionId,
]);
$ch = curl_init('http://127.0.0.1:7474/job');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $body,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_TIMEOUT => 1,
CURLOPT_CONNECTTIMEOUT => 1,
]);
@curl_exec($ch);
curl_close($ch);
}
// Helper: submit job to Arc Reactor // Helper: submit job to Arc Reactor
function arcPost(string $path, array $body): ?array { function arcPost(string $path, array $body): ?array {
$ch = curl_init('http://127.0.0.1:7474' . $path); $ch = curl_init('http://127.0.0.1:7474' . $path);
@@ -1481,6 +1527,71 @@ if (!$reply) {
} }
} }
// ── Tier 0.9k: Memory Core — remember/forget/recall voice commands ───────────
if (!$reply) {
// "remember that I prefer X", "remember X", "note that X"
if (preg_match('/^(?:jarvis[,\s]+)?(?:remember|note down|make a note|remember that|note that)\s+(?:that\s+)?(.+)/i', $message, $m)) {
$fact = trim($m[1]);
// Use LLM to parse the fact into subject/predicate/object — but for speed, use heuristic
$arcRes = arcPost('/job', [
'type' => 'memory_store',
'payload' => ['subject' => 'user', 'predicate' => 'note', 'object' => $fact, 'category' => 'instruction'],
'priority' => 8,
'created_by' => 'chat:' . $sessionId,
]);
$reply = "Noted, {$userAddr}. I'll remember that: {$fact}";
$source = 'memory:store';
}
}
if (!$reply) {
// "forget X", "forget that X", "remove memory X"
if (preg_match('/^(?:jarvis[,\s]+)?(?:forget|discard|remove|delete)\s+(?:that\s+|the\s+memory\s+(?:about\s+)?)?(.+)/i', $message, $m)) {
$keyword = trim($m[1]);
// Mark matching facts inactive
$affected = JarvisDB::execute(
"UPDATE memory_facts SET active=0 WHERE active=1 AND (subject LIKE ? OR object LIKE ? OR predicate LIKE ?)",
["%{$keyword}%", "%{$keyword}%", "%{$keyword}%"]
);
$reply = $affected
? "Memory cleared, {$userAddr}. Removed facts related to: {$keyword}."
: "Nothing in memory matched \"{$keyword}\", {$userAddr}.";
$source = 'memory:forget';
}
}
if (!$reply) {
// "what do you know about X", "what do you remember about X"
if (preg_match('/^(?:jarvis[,\s]+)?(?:what do you know about|what do you remember about|recall|memory about)\s+(.+)/i', $message, $m)) {
$keyword = trim($m[1]);
$facts = JarvisDB::query(
"SELECT * FROM memory_facts WHERE active=1 AND (subject LIKE ? OR object LIKE ?) ORDER BY confirmed_count DESC LIMIT 8",
["%{$keyword}%", "%{$keyword}%"]
) ?: [];
if (empty($facts)) {
$reply = "I don't have any stored memories about \"{$keyword}\", {$userAddr}.";
} else {
$lines = array_map(fn($f) => "{$f['subject']} {$f['predicate']}: {$f['object']}", $facts);
$reply = "◈ I know the following about \"{$keyword}\", {$userAddr}:\n" . implode("\n", $lines);
}
$source = 'memory:recall';
}
}
if (!$reply) {
// "how many memories", "memory status", "what do you know about me"
if (preg_match('/^(?:jarvis[,\s]+)?(?:(?:how many|show)\s+memories|memory\s+(?:status|count|summary)|what do you know about me)/i', $message)) {
$stats = JarvisDB::query(
"SELECT category, COUNT(*) cnt FROM memory_facts WHERE active=1 GROUP BY category ORDER BY cnt DESC"
) ?: [];
$total = array_sum(array_column($stats, 'cnt'));
if (!$total) {
$reply = "My memory core is empty, {$userAddr}. I'll start learning from our conversations.";
} else {
$breakdown = implode(', ', array_map(fn($s) => "{$s['cnt']} {$s['category']}", $stats));
$reply = "◈ Memory Core: {$total} facts stored — {$breakdown}. I use these to personalize responses.";
}
$source = 'memory:status';
}
}
// ── Tier 1: Intent Engine (instant, no LLM) ─────────────────────────────── // ── Tier 1: Intent Engine (instant, no LLM) ───────────────────────────────
if (!$reply) { if (!$reply) {
$matched = KBEngine::match($message); $matched = KBEngine::match($message);
@@ -1612,6 +1723,12 @@ if (!$reply) {
} }
// ── Memory injection — fetch relevant facts before LLM tiers ─────────────
$memoryContext = '';
if (!$reply) {
$memoryContext = getMemoryContext($message, 12);
}
// ── Tier 2: Ollama local LLM (fast local fallback) ─────────────────────── // ── Tier 2: Ollama local LLM (fast local fallback) ───────────────────────
if (!$reply && defined('OLLAMA_HOST') && OLLAMA_HOST) { if (!$reply && defined('OLLAMA_HOST') && OLLAMA_HOST) {
$ollamaHost = OLLAMA_HOST; $ollamaHost = OLLAMA_HOST;
@@ -1672,10 +1789,12 @@ if (!$reply && defined('GROQ_API_KEY') && GROQ_API_KEY) {
); );
$groqModel = $needsSearch ? GROQ_MODEL_SEARCH : GROQ_MODEL_GENERAL; $groqModel = $needsSearch ? GROQ_MODEL_SEARCH : GROQ_MODEL_GENERAL;
$memSuffix = $memoryContext ? "\n\n{$memoryContext}" : '';
$groqMessages = [['role' => 'system', 'content' => $groqMessages = [['role' => 'system', 'content' =>
"You are JARVIS — Just A Rather Very Intelligent System — the AI of {$userName} " . "You are JARVIS — Just A Rather Very Intelligent System — the AI of {$userName} " .
"(address him as \"{$userAddr}\"). Formal, efficient, British butler tone. " . "(address him as \"{$userAddr}\"). Formal, efficient, British butler tone. " .
'Be concise — 2-4 sentences unless detail is explicitly requested. Today: ' . date('D M j Y g:i A T') . '.'], 'Be concise — 2-4 sentences unless detail is explicitly requested. Today: ' . date('D M j Y g:i A T') .
$memSuffix . '.'],
]; ];
foreach (array_slice($history, -6) as $h) { foreach (array_slice($history, -6) as $h) {
$groqMessages[] = ['role' => $h['role'], 'content' => $h['content']]; $groqMessages[] = ['role' => $h['role'], 'content' => $h['content']];
@@ -1757,7 +1876,8 @@ Infrastructure:
- Network: 10.48.200.0/24, FortiGate firewall - Network: 10.48.200.0/24, FortiGate firewall
Live data: Live data:
{$systemContext}" . ($kbContext ? "\nKnowledge base:\n{$kbContext}" : '') . " {$systemContext}" . ($kbContext ? "\nKnowledge base:\n{$kbContext}" : '') .
($memoryContext ? "\n\n{$memoryContext}" : '') . "
Today: " . date('l, F j Y, g:i A T') . " Today: " . date('l, F j Y, g:i A T') . "
Respond as JARVIS. Voice readout: under 3 sentences unless detail is requested. For system status, interpret the data and give an assessment — don't just recite numbers."; Respond as JARVIS. Voice readout: under 3 sentences unless detail is requested. For system status, interpret the data and give an assessment — don't just recite numbers.";
@@ -1827,6 +1947,11 @@ JarvisDB::insert(
); );
KBEngine::learnFromConversation($message, $reply); KBEngine::learnFromConversation($message, $reply);
// Memory Core — async extraction for LLM responses (don't extract from intent/KB/fallback)
if ($reply && !in_array(explode(':', $source)[0], ['intent', 'kb', 'fallback', 'memory', 'arc'])) {
memoryExtractAsync($message, $reply, $sessionId);
}
echo json_encode([ echo json_encode([
'reply' => $reply, 'reply' => $reply,
'source' => $source, 'source' => $source,
+81
View File
@@ -0,0 +1,81 @@
<?php
/**
* JARVIS Memory Core API
*/
require_once __DIR__ . '/../../api/lib/db.php';
$action = $_GET['action'] ?? '';
$data = json_decode(file_get_contents('php://input'), true) ?? [];
function memArcGet(string $path): array {
$ch = curl_init('http://127.0.0.1:7474' . $path);
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER=>true, CURLOPT_TIMEOUT=>5]);
$raw = curl_exec($ch); curl_close($ch);
return json_decode($raw, true) ?: [];
}
function memArcPost(string $path, array $body): array {
$ch = curl_init('http://127.0.0.1:7474' . $path);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER=>true, CURLOPT_POST=>true, CURLOPT_TIMEOUT=>5,
CURLOPT_POSTFIELDS => json_encode($body),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
]);
$raw = curl_exec($ch); curl_close($ch);
return json_decode($raw, true) ?: [];
}
function memArcDelete(string $path): array {
$ch = curl_init('http://127.0.0.1:7474' . $path);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER=>true, CURLOPT_CUSTOMREQUEST=>'DELETE', CURLOPT_TIMEOUT=>5,
]);
$raw = curl_exec($ch); curl_close($ch);
return json_decode($raw, true) ?: ['ok' => true];
}
switch ($action) {
case 'list':
$limit = min((int)($_GET['limit'] ?? 100), 500);
$category = $_GET['category'] ?? '';
$search = $_GET['search'] ?? '';
$qs = http_build_query(array_filter(['limit'=>$limit,'category'=>$category,'search'=>$search]));
echo json_encode(memArcGet('/memory/facts' . ($qs ? '?'.$qs : '')));
break;
case 'stats':
echo json_encode(memArcGet('/memory/stats'));
break;
case 'context':
$msg = $_GET['message'] ?? '';
$limit = (int)($_GET['limit'] ?? 12);
$qs = http_build_query(['message'=>$msg,'limit'=>$limit]);
echo json_encode(memArcGet('/memory/context?' . $qs));
break;
case 'store':
$subject = trim($data['subject'] ?? '');
$predicate = trim($data['predicate'] ?? 'is');
$object = trim($data['object'] ?? '');
$category = $data['category'] ?? 'fact';
if (!$subject || !$object) { http_response_code(400); echo json_encode(['error'=>'subject and object required']); break; }
echo json_encode(memArcPost('/memory/facts', ['subject'=>$subject,'predicate'=>$predicate,'object'=>$object,'category'=>$category]));
break;
case 'delete':
$id = (int)($_GET['id'] ?? 0);
if (!$id) { http_response_code(400); echo json_encode(['error'=>'Missing id']); break; }
echo json_encode(memArcDelete('/memory/facts/' . $id));
break;
case 'clear':
$category = $_GET['category'] ?? '';
$qs = $category ? '?category=' . urlencode($category) : '';
echo json_encode(memArcDelete('/memory/facts' . $qs));
break;
default:
http_response_code(404);
echo json_encode(['error' => 'Unknown memory action: ' . $action]);
}
+223
View File
@@ -833,6 +833,49 @@ if ($action) {
$raw = curl_exec($ch); curl_close($ch); $raw = curl_exec($ch); curl_close($ch);
j(json_decode($raw, true) ?: ['ok'=>true]); j(json_decode($raw, true) ?: ['ok'=>true]);
// ── MEMORY CORE ──────────────────────────────────────────────────────
case 'memory_list':
$limit = min((int)($_GET['limit'] ?? 200), 500);
$category = $_GET['category'] ?? '';
$search = $_GET['search'] ?? '';
$qs = http_build_query(array_filter(['limit'=>$limit,'category'=>$category,'search'=>$search]));
$ch = curl_init('http://127.0.0.1:7474/memory/facts' . ($qs ? '?'.$qs : ''));
curl_setopt_array($ch,[CURLOPT_RETURNTRANSFER=>true,CURLOPT_TIMEOUT=>5]);
$raw = curl_exec($ch); curl_close($ch);
j(json_decode($raw,true) ?: []);
case 'memory_stats':
$ch = curl_init('http://127.0.0.1:7474/memory/stats');
curl_setopt_array($ch,[CURLOPT_RETURNTRANSFER=>true,CURLOPT_TIMEOUT=>5]);
$raw = curl_exec($ch); curl_close($ch);
j(json_decode($raw,true) ?: ['total'=>0,'by_category'=>[]]);
case 'memory_store':
$body = json_decode(file_get_contents('php://input'),true) ?: [];
$ch = curl_init('http://127.0.0.1:7474/memory/facts');
curl_setopt_array($ch,[
CURLOPT_RETURNTRANSFER=>true, CURLOPT_POST=>true, CURLOPT_TIMEOUT=>5,
CURLOPT_POSTFIELDS => json_encode($body),
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
]);
$raw = curl_exec($ch); curl_close($ch);
j(json_decode($raw,true) ?: ['ok'=>true]);
case 'memory_delete':
$id = (int)($_GET['id'] ?? 0); if (!$id) bad('Missing id');
$ch = curl_init('http://127.0.0.1:7474/memory/facts/' . $id);
curl_setopt_array($ch,[CURLOPT_RETURNTRANSFER=>true,CURLOPT_CUSTOMREQUEST=>'DELETE',CURLOPT_TIMEOUT=>5]);
curl_exec($ch); curl_close($ch);
j(['ok'=>true]);
case 'memory_clear':
$category = $_GET['category'] ?? '';
$url = 'http://127.0.0.1:7474/memory/facts' . ($category ? '?category=' . urlencode($category) : '');
$ch = curl_init($url);
curl_setopt_array($ch,[CURLOPT_RETURNTRANSFER=>true,CURLOPT_CUSTOMREQUEST=>'DELETE',CURLOPT_TIMEOUT=>5]);
curl_exec($ch); curl_close($ch);
j(['ok'=>true]);
// ── VISION PROTOCOL ────────────────────────────────────────────────── // ── VISION PROTOCOL ──────────────────────────────────────────────────
case 'vision_list': case 'vision_list':
$limit = min((int)($_GET['limit'] ?? 30), 100); $limit = min((int)($_GET['limit'] ?? 30), 100);
@@ -1171,6 +1214,7 @@ select.filter-sel:focus{border-color:var(--cyan)}
<div class="nav-item" data-tab="missions" onclick="nav(this)">◈ MISSION OPS</div> <div class="nav-item" data-tab="missions" onclick="nav(this)">◈ MISSION OPS</div>
<div class="nav-item" data-tab="directives" onclick="nav(this)">◈ DIRECTIVES</div> <div class="nav-item" data-tab="directives" onclick="nav(this)">◈ DIRECTIVES</div>
<div class="nav-item" data-tab="clearance" onclick="nav(this)" id="nav-clearance">🔒 CLEARANCE</div> <div class="nav-item" data-tab="clearance" onclick="nav(this)" id="nav-clearance">🔒 CLEARANCE</div>
<div class="nav-item" data-tab="memory" onclick="nav(this)" id="nav-memory">◈ MEMORY CORE</div>
<div class="nav-section">INFO</div> <div class="nav-section">INFO</div>
<div class="nav-item" data-tab="sites" onclick="nav(this)">SITES</div> <div class="nav-item" data-tab="sites" onclick="nav(this)">SITES</div>
<div class="nav-item" data-tab="users" onclick="nav(this)">USERS</div> <div class="nav-item" data-tab="users" onclick="nav(this)">USERS</div>
@@ -1778,6 +1822,64 @@ select.filter-sel:focus{border-color:var(--cyan)}
</div> </div>
</div> </div>
<!-- MEMORY CORE -->
<div class="tab" id="tab-memory">
<div class="page-title">◈ MEMORY CORE — KNOWLEDGE GRAPH</div>
<div style="display:flex;gap:10px;margin-bottom:16px;align-items:center;flex-wrap:wrap">
<button class="btn btn-sm btn-green" onclick="memoryNew()">+ ADD FACT</button>
<button class="btn btn-sm" onclick="loadMemory()">↻ REFRESH</button>
<select id="mem-cat-filter" class="inp" style="width:auto;padding:4px 8px;font-size:0.65rem" onchange="loadMemory()">
<option value="">ALL CATEGORIES</option>
<option value="preference">PREFERENCE</option>
<option value="person">PERSON</option>
<option value="place">PLACE</option>
<option value="routine">ROUTINE</option>
<option value="goal">GOAL</option>
<option value="fact">FACT</option>
<option value="instruction">INSTRUCTION</option>
</select>
<input id="mem-search" class="inp" placeholder="Search..." style="width:160px;padding:4px 8px;font-size:0.65rem" oninput="loadMemory()">
<button class="btn btn-sm btn-red" onclick="memoryClearAll()">CLEAR ALL</button>
<div id="memory-stats-bar" style="margin-left:auto;font-family:var(--mono);font-size:0.65rem;color:var(--dim)"></div>
</div>
<div id="memory-list"><div class="loading">LOADING MEMORY CORE...</div></div>
<!-- Add fact panel -->
<div id="memory-editor" style="display:none;margin-top:20px;border:1px solid var(--border);border-radius:6px;padding:16px;background:rgba(0,212,255,0.02)">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
<div style="font-family:var(--mono);font-size:0.7rem;letter-spacing:2px;color:var(--cyan)">◈ ADD MEMORY FACT</div>
<button class="btn btn-xs" onclick="document.getElementById('memory-editor').style.display='none'">✕</button>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr 1fr 1fr;gap:8px;margin-bottom:10px">
<div>
<div class="lbl">CATEGORY</div>
<select id="mem-new-cat" class="inp">
<option value="fact">FACT</option>
<option value="preference">PREFERENCE</option>
<option value="person">PERSON</option>
<option value="place">PLACE</option>
<option value="routine">ROUTINE</option>
<option value="goal">GOAL</option>
<option value="instruction">INSTRUCTION</option>
</select>
</div>
<div>
<div class="lbl">SUBJECT</div>
<input id="mem-new-subject" class="inp" placeholder="e.g. user, Tom">
</div>
<div>
<div class="lbl">PREDICATE</div>
<input id="mem-new-predicate" class="inp" placeholder="e.g. prefers, works at">
</div>
<div>
<div class="lbl">VALUE</div>
<input id="mem-new-object" class="inp" placeholder="e.g. dark mode">
</div>
</div>
<button class="btn btn-sm btn-green" onclick="memorySave()">◈ SAVE FACT</button>
</div>
</div>
</div><!-- /content --> </div><!-- /content -->
</div><!-- /main --> </div><!-- /main -->
</div><!-- /app --> </div><!-- /app -->
@@ -1894,6 +1996,7 @@ function loadTab(tab) {
missions: loadMissions, missions: loadMissions,
directives: loadDirectives, directives: loadDirectives,
clearance: loadClearance, clearance: loadClearance,
memory: loadMemory,
vision: loadVision, vision: loadVision,
guardian: loadGuardian, guardian: loadGuardian,
tasks: loadTasks, tasks: loadTasks,
@@ -3942,6 +4045,126 @@ async function syncCalNow() {
}); });
} }
// ── MEMORY CORE ───────────────────────────────────────────────────────────────
const CAT_COLORS = {
preference:'var(--cyan)', person:'#a78bfa', place:'#00ff88',
routine:'#ffd700', goal:'#ff9900', fact:'var(--text)', instruction:'#ff6680'
};
async function loadMemory() {
const el = document.getElementById('memory-list');
const cat = document.getElementById('mem-cat-filter').value;
const search = document.getElementById('mem-search').value;
el.innerHTML = '<div class="loading">LOADING...</div>';
const [facts, stats] = await Promise.all([
api('memory_list' + (cat ? '&category='+encodeURIComponent(cat) : '') + (search ? '&search='+encodeURIComponent(search) : '') + '&limit=200'),
api('memory_stats'),
]);
const list = Array.isArray(facts) ? facts : [];
const s = stats || {};
const bar = document.getElementById('memory-stats-bar');
if (bar) {
const cats = (s.by_category||[]).map(c=>`${c.cnt} ${c.category}`).join(' · ');
bar.textContent = `${s.total||0} FACTS${cats ? ' — ' + cats : ''}`;
}
if (!list.length) {
el.innerHTML = '<div class="empty-state">No memory facts yet. Start chatting with JARVIS — I auto-learn from conversations.</div>';
return;
}
// Group by category
const grouped = {};
for (const f of list) {
if (!grouped[f.category]) grouped[f.category] = [];
grouped[f.category].push(f);
}
let html = '';
const catOrder = ['instruction','preference','person','place','routine','goal','fact'];
const allCats = [...new Set([...catOrder, ...Object.keys(grouped)])];
for (const cat of allCats) {
if (!grouped[cat]) continue;
const color = CAT_COLORS[cat] || 'var(--text)';
html += `<div style="margin-bottom:16px">
<div style="font-family:var(--mono);font-size:0.65rem;letter-spacing:2px;color:${color};margin-bottom:6px;display:flex;align-items:center;gap:8px">
${cat.toUpperCase()} <span style="color:var(--dim)">(${grouped[cat].length})</span>
<button class="btn btn-xs btn-red" onclick="memoryClearCategory('${cat}')" style="margin-left:auto">CLEAR</button>
</div>
<table class="tbl"><thead><tr><th>SUBJECT</th><th>PREDICATE</th><th>VALUE</th><th>CONFIDENCE</th><th>CONFIRMED</th><th>SOURCE</th><th>LAST SEEN</th><th></th></tr></thead><tbody>`;
for (const f of grouped[cat]) {
const conf = (parseFloat(f.confidence)*100).toFixed(0)+'%';
const ts = f.last_confirmed_at ? new Date(f.last_confirmed_at).toLocaleDateString() : '';
const srcColor = f.source === 'explicit' ? 'var(--green)' : f.source === 'inference' ? 'var(--yellow)' : 'var(--dim)';
html += `<tr>
<td style="font-family:var(--mono);font-size:0.65rem;color:${color}">${esc(f.subject)}</td>
<td style="font-family:var(--mono);font-size:0.6rem;color:var(--dim)">${esc(f.predicate)}</td>
<td style="font-family:var(--mono);font-size:0.6rem">${esc(f.object)}</td>
<td style="font-family:var(--mono);font-size:0.6rem">${conf}</td>
<td style="font-family:var(--mono);font-size:0.6rem;text-align:center">${f.confirmed_count}</td>
<td style="font-family:var(--mono);font-size:0.55rem;color:${srcColor}">${f.source}</td>
<td style="font-family:var(--mono);font-size:0.55rem;color:var(--dim)">${ts}</td>
<td><button class="btn btn-xs btn-red" onclick="memoryDelete(${f.id})">DEL</button></td>
</tr>`;
}
html += '</tbody></table></div>';
}
el.innerHTML = html;
}
function memoryNew() {
document.getElementById('memory-editor').style.display = 'block';
document.getElementById('mem-new-subject').focus();
}
async function memorySave() {
const body = {
category: document.getElementById('mem-new-cat').value,
subject: document.getElementById('mem-new-subject').value.trim(),
predicate: document.getElementById('mem-new-predicate').value.trim() || 'is',
object: document.getElementById('mem-new-object').value.trim(),
};
if (!body.subject || !body.object) { toast('Subject and value required','err'); return; }
try {
const r = await fetch(location.href + '?action=memory_store', {
method: 'POST', headers: {'Content-Type':'application/json'}, body: JSON.stringify(body)
});
const d = await r.json();
if (d.ok) {
toast('Fact stored', 'ok');
document.getElementById('mem-new-subject').value = '';
document.getElementById('mem-new-predicate').value = '';
document.getElementById('mem-new-object').value = '';
document.getElementById('memory-editor').style.display = 'none';
loadMemory();
} else { toast('Error: '+(d.detail||d.error||'unknown'),'err'); }
} catch(e) { toast('Failed','err'); }
}
async function memoryDelete(id) {
if (!confirm('Delete this memory fact?')) return;
await fetch(location.href + '?action=memory_delete&id=' + id, {method:'POST'});
toast('Deleted','ok');
loadMemory();
}
async function memoryClearCategory(cat) {
if (!confirm('Clear all ' + cat + ' memories?')) return;
await fetch(location.href + '?action=memory_clear&category=' + encodeURIComponent(cat), {method:'POST'});
toast('Cleared ' + cat + ' memories', 'ok');
loadMemory();
}
async function memoryClearAll() {
if (!confirm('Clear ALL memory facts? This cannot be undone.')) return;
if (!confirm('Are you absolutely sure? All JARVIS memories will be deleted.')) return;
await fetch(location.href + '?action=memory_clear', {method:'POST'});
toast('All memories cleared', 'ok');
loadMemory();
}
// ── CLEARANCE PROTOCOL ──────────────────────────────────────────────────────── // ── CLEARANCE PROTOCOL ────────────────────────────────────────────────────────
async function loadClearance() { async function loadClearance() {
const [pending, rules, history] = await Promise.all([ const [pending, rules, history] = await Promise.all([
+3
View File
@@ -105,6 +105,9 @@ switch ($endpoint) {
case "directives": case "directives":
require __DIR__ . "/../api/endpoints/directives.php"; require __DIR__ . "/../api/endpoints/directives.php";
break; break;
case "memory":
require __DIR__ . "/../api/endpoints/memory.php";
break;
case "calendar": case "calendar":
require __DIR__ . '/../api/endpoints/calendar_sync.php'; require __DIR__ . '/../api/endpoints/calendar_sync.php';
break; break;
+24
View File
@@ -1322,6 +1322,11 @@ body::after{
<span id="bb-guardian-badge" style="display:none;background:var(--red);color:#fff;font-size:0.45rem;padding:1px 4px;border-radius:2px;font-family:var(--font-mono);letter-spacing:0">0</span> <span id="bb-guardian-badge" style="display:none;background:var(--red);color:#fff;font-size:0.45rem;padding:1px 4px;border-radius:2px;font-family:var(--font-mono);letter-spacing:0">0</span>
</div> </div>
<div class="bb-item" style="cursor:pointer" onclick="switchTab('clearance')" id="bb-memory-item">
<div class="bb-dot" id="bb-memory-dot" style="background:rgba(0,212,255,0.3)"></div>
<span>MEMORY</span> <span id="bb-memory-count" style="color:var(--text-dim)">--</span>
</div>
<div id="ekgWrap" style="margin-left:auto"><canvas id="ekgCanvas"></canvas></div> <div id="ekgWrap" style="margin-left:auto"><canvas id="ekgCanvas"></canvas></div>
<div style="font-size:0.65rem;flex-shrink:0"> <div style="font-size:0.65rem;flex-shrink:0">
JARVIS v2.0 · SECURITY LEVEL ALPHA · UPDATED <span id="last-refresh">--:--:--</span> JARVIS v2.0 · SECURITY LEVEL ALPHA · UPDATED <span id="last-refresh">--:--:--</span>
@@ -2508,6 +2513,11 @@ function showApp(name, greeting, silent = false) {
updateClearanceBanner(); updateClearanceBanner();
setInterval(updateClearanceBanner, 30000); setInterval(updateClearanceBanner, 30000);
}, 6000); }, 6000);
// Memory Core — poll count every 60s
setTimeout(() => {
updateMemoryCount();
setInterval(updateMemoryCount, 60000);
}, 8000);
} }
async function logout() { async function logout() {
@@ -4408,6 +4418,20 @@ async function hudDirectiveReview(id) {
} }
} }
// ── MEMORY CORE — bottom bar count ────────────────────────────────────────────
async function updateMemoryCount() {
try {
const stats = await api('memory?action=stats');
const el = document.getElementById('bb-memory-count');
const dot = document.getElementById('bb-memory-dot');
if (el && stats) {
const total = stats.total || 0;
el.textContent = total + ' FACTS';
if (dot) dot.style.background = total > 0 ? 'var(--cyan)' : 'rgba(0,212,255,0.3)';
}
} catch(e) {}
}
// ── CLEARANCE PROTOCOL HUD ───────────────────────────────────────────────────── // ── CLEARANCE PROTOCOL HUD ─────────────────────────────────────────────────────
const _clrOpenCards = new Set(); const _clrOpenCards = new Set();