diff --git a/dashboard/api.js b/dashboard/api.js
index 092a5a7..a25f498 100644
--- a/dashboard/api.js
+++ b/dashboard/api.js
@@ -93,6 +93,10 @@ const api = {
recomputeAgentTime: () => api.post('/api/agent-time/recompute', {}),
// Agent Insights (AI usage analytics)
getAgentInsights: (recompute = false) => api.get(`/api/agent-insights${recompute ? '?recompute=true' : ''}`),
+ // Brain Index (centralized knowledge pipeline)
+ getBrainIndexSearch: (q, source = '', limit = 20) => api.get(`/api/brain-index/search?q=${encodeURIComponent(q)}&limit=${limit}${source ? `&source=${encodeURIComponent(source)}` : ''}`),
+ getBrainIndexStats: () => api.get('/api/brain-index/stats'),
+ brainIndexIngest: () => api.post('/api/brain-index/ingest', {}),
// Agent Registry
getAgents: () => api.get('/api/agents'),
registerAgent: (data) => api.post('/api/agents/register', data),
diff --git a/dashboard/index.html b/dashboard/index.html
index da7abd9..d126e21 100644
--- a/dashboard/index.html
+++ b/dashboard/index.html
@@ -50,6 +50,7 @@
π§Smart Router
πLearning Analytics
π§ Agent Insights
+ πBrain Search
πSession Replay
β±Agent Time
diff --git a/dashboard/pages/brain-search.js b/dashboard/pages/brain-search.js
new file mode 100644
index 0000000..a810512
--- /dev/null
+++ b/dashboard/pages/brain-search.js
@@ -0,0 +1,91 @@
+// Brain Search β query the centralized brain index (brain/ notes,
+// skill learnings, chat history) via /api/brain-index/search.
+
+async function renderBrainSearch() {
+ const content = document.getElementById('pageContent');
+ content.innerHTML = `
+
+
+
+
+
+
+
+
+
+ π
Search your brain
Type a query above to search across all indexed knowledge.
+ `;
+ // Load stats + initial hint.
+ try {
+ const s = await api.getBrainIndexStats();
+ const parts = Object.entries(s)
+ .filter(([k]) => k !== 'total')
+ .map(([k, v]) => `${k}: ${v}`);
+ document.getElementById('brainStats').textContent =
+ `Indexed: ${s.total || 0} docs (${parts.join(' Β· ')})`;
+ } catch (e) {
+ document.getElementById('brainStats').textContent = 'Index not built yet β click Re-ingest.';
+ }
+}
+
+async function brainSearchRun() {
+ const q = document.getElementById('brainQuery').value.trim();
+ const source = document.getElementById('brainSource').value;
+ const box = document.getElementById('brainResults');
+ if (!q) { box.innerHTML = ''; return; }
+ box.innerHTML = '';
+ try {
+ const data = await api.getBrainIndexSearch(q, source);
+ if (!data.results.length) {
+ box.innerHTML = 'π
No results
Nothing matched "' + escapeHtml(q) + '"' + (source ? ' in ' + escapeHtml(source) : '') + '.
';
+ return;
+ }
+ box.innerHTML = `${data.count} result(s) for "${escapeHtml(q)}"
` +
+ data.results.map(r => {
+ const srcIcon = { 'brain': 'π', 'skill-learning': 'π§ ', 'chat': 'π¬', 'agent-note': 'π€' }[r.source] || 'π';
+ const agent = r.agent ? `${escapeHtml(r.agent)}` : '';
+ return `
+
+
+ ${srcIcon}
+ ${escapeHtml(r.title)}
+ ${escapeHtml(r.source)}
+ ${agent}
+
+
${escapeHtml(r.snippet || '')}
+
`;
+ }).join('');
+ } catch (err) {
+ box.innerHTML = `β
Search failed
${escapeHtml(err.message)}
`;
+ }
+}
+
+async function brainSearchIngest() {
+ const btn = event && event.target;
+ if (btn) { btn.disabled = true; btn.textContent = 'Ingestingβ¦'; }
+ try {
+ const r = await api.brainIndexIngest();
+ const s = r.stats || {};
+ if (btn) btn.textContent = 'β Re-ingest';
+ const statEl = document.getElementById('brainStats');
+ if (statEl) {
+ const parts = Object.entries(s).filter(([k]) => k !== 'total').map(([k, v]) => `${k}: ${v}`);
+ statEl.textContent = `Indexed: ${s.total || 0} docs (${parts.join(' Β· ')})`;
+ }
+ } catch (e) {
+ if (btn) btn.textContent = 'Ingest failed';
+ } finally {
+ if (btn) setTimeout(() => { btn.disabled = false; btn.textContent = 'β³ Re-ingest'; }, 1500);
+ }
+}
diff --git a/dashboard/utils.js b/dashboard/utils.js
index da28eec..6a59491 100644
--- a/dashboard/utils.js
+++ b/dashboard/utils.js
@@ -118,6 +118,7 @@ const PAGE_TITLES = {
audit: { title: 'Audit Log', breadcrumb: 'System activity trail' },
cost: { title: 'Cost Analytics', breadcrumb: 'Usage & spending' },
'agent-insights': { title: 'Agent Insights', breadcrumb: 'AI usage analytics' },
+ 'brain-search': { title: 'Brain Search', breadcrumb: 'Unified knowledge index' },
plugins: { title: 'Plugin Registry', breadcrumb: 'Manage plugins' },
backups: { title: 'Backups', breadcrumb: 'Disaster recovery' },
prompts: { title: 'Prompt Library', breadcrumb: 'Reusable templates' },
diff --git a/scheduler/jobs/brain-index-ingest-job.json b/scheduler/jobs/brain-index-ingest-job.json
new file mode 100644
index 0000000..c99be09
--- /dev/null
+++ b/scheduler/jobs/brain-index-ingest-job.json
@@ -0,0 +1,10 @@
+{
+ "id": "brain-index-ingest",
+ "name": "Brain Index Auto-Ingest",
+ "endpoint": "/api/brain-index/ingest",
+ "cron": "*/30 * * * *",
+ "enabled": true,
+ "created": "2026-07-25T00:00:00Z",
+ "last_run": null,
+ "next_run": null
+}
diff --git a/scheduler/scheduler.py b/scheduler/scheduler.py
index f27dd35..d7ca2b5 100644
--- a/scheduler/scheduler.py
+++ b/scheduler/scheduler.py
@@ -49,7 +49,7 @@ def log_audit(entry: dict):
def run_skill_via_api(skill_name: str, agent: str = "auto") -> dict:
"""Invoke a skill through the live server API.
- Returns a dict with at least {'ok': bool, 'reason'/'output': ...}.
+ Returns a dict with at least {'ok': bool, 'reason'/'data': ...}.
"""
url = f"{SERVER_URL}/api/skills/{skill_name}/run"
payload = json.dumps({"input": "", "agent": agent}).encode()
@@ -68,32 +68,58 @@ def run_skill_via_api(skill_name: str, agent: str = "auto") -> dict:
return {"ok": False, "reason": str(e)}
-def run_job(job: dict):
- skill = job.get("skill")
- name = job.get("name", skill)
- agent = job.get("agent", "auto")
- print(f"[{datetime.now().isoformat()}] Firing job '{name}' -> skill '{skill}'")
- log_audit({"action": "scheduler_run", "job": name, "skill": skill, "stage": "start"})
+def run_endpoint_via_api(endpoint: str) -> dict:
+ """POST to an arbitrary server endpoint (e.g. /api/brain-index/ingest)
+ so scheduler jobs can trigger maintenance tasks that aren't skills."""
+ url = f"{SERVER_URL}{endpoint}"
+ req = urllib.request.Request(
+ url, data=b"{}", headers={"Content-Type": "application/json"}, method="POST"
+ )
+ try:
+ with urllib.request.urlopen(req, timeout=RUN_TIMEOUT) as resp:
+ data = json.loads(resp.read())
+ return {"ok": True, "data": data}
+ except urllib.error.HTTPError as e:
+ return {"ok": False, "reason": f"HTTP {e.code}: {e.reason}"}
+ except urllib.error.URLError as e:
+ return {"ok": False, "reason": f"server unreachable: {e.reason}"}
+ except Exception as e:
+ return {"ok": False, "reason": str(e)}
- if not skill:
- log_audit({"action": "scheduler_run", "job": name, "error": "no skill defined"})
+
+def run_job(job: dict):
+ name = job.get("name", job.get("skill") or job.get("endpoint"))
+ agent = job.get("agent", "auto")
+ endpoint = job.get("endpoint")
+ skill = job.get("skill")
+ print(f"[{datetime.now().isoformat()}] Firing job '{name}' -> "
+ f"{('endpoint ' + endpoint) if endpoint else ('skill ' + str(skill))}")
+ log_audit({"action": "scheduler_run", "job": name,
+ "skill": skill, "endpoint": endpoint, "stage": "start"})
+
+ if endpoint:
+ result = run_endpoint_via_api(endpoint)
+ elif skill:
+ result = run_skill_via_api(skill, agent)
+ else:
+ log_audit({"action": "scheduler_run", "job": name, "error": "no skill or endpoint defined"})
return
- result = run_skill_via_api(skill, agent)
if result["ok"]:
- data = result["data"]
+ data = result.get("data", {})
log_audit({
"action": "scheduler_run",
"job": name,
"skill": skill,
- "agent": data.get("agent"),
- "run_id": data.get("run_id"),
+ "endpoint": endpoint,
+ "result": data,
"stage": "done",
})
- print(f" -> OK (agent={data.get('agent')}, run_id={data.get('run_id')})")
+ print(f" -> OK")
else:
reason = result["reason"]
- log_audit({"action": "scheduler_run", "job": name, "skill": skill, "error": reason})
+ log_audit({"action": "scheduler_run", "job": name,
+ "skill": skill, "endpoint": endpoint, "error": reason})
print(f" -> FAILED: {reason}")
@@ -112,20 +138,22 @@ def load_jobs(scheduler: BackgroundScheduler):
continue
cron = data.get("cron")
skill = data.get("skill")
- if not cron or not skill:
- print(f" Skipping job {data.get('name')}: missing cron or skill")
+ endpoint = data.get("endpoint")
+ if not cron or not (skill or endpoint):
+ print(f" Skipping job {data.get('name')}: missing cron and (skill or endpoint)")
continue
scheduler.add_job(
run_job,
CronTrigger.from_crontab(cron),
args=[data],
id=data.get("id", data["name"]),
- name=data.get("name", skill),
+ name=data.get("name", skill or endpoint),
replace_existing=True,
max_instances=1,
coalesce=True,
)
- print(f" Scheduled: {data.get('name')} (skill={skill}, cron={cron})")
+ print(f" Scheduled: {data.get('name')} "
+ f"({'endpoint=' + endpoint if endpoint else 'skill=' + str(skill)}, cron={cron})")
def main():
diff --git a/server.py b/server.py
index fa5df21..1c77fd1 100644
--- a/server.py
+++ b/server.py
@@ -187,6 +187,34 @@ def append_audit(entry: dict):
# underlying operation, but surface it on the server console.
print(f"[audit] failed to write entry {entry.get('action')!r}: {e}")
+
+def record_brain_learning(source: str, source_path: str, title: str,
+ content: str, agent: str | None = None,
+ updated_at: str | None = None) -> bool:
+ """Push a single document into the centralized brain index.
+
+ Best-effort: a brain-index failure must never break the calling
+ operation (e.g. a skill run). Calls the same engine the CLI/HTTP
+ endpoints use, synchronously β no HTTP round-trip.
+ """
+ try:
+ mod = _brain_index_module()
+ conn = mod.get_conn()
+ try:
+ mod.upsert_doc(
+ conn, source=source, source_path=source_path, title=title,
+ content=content, agent=agent, updated_at=updated_at,
+ )
+ conn.commit()
+ return True
+ finally:
+ conn.close()
+ except Exception as e:
+ print(f"[brain-index] learning upsert failed ({source}/{source_path}): {e}")
+ return False
+
+
+
# βββ Agent Stats (real, persisted) ββββββββββββββββββββββββββββββ
AGENT_STATS_FILE = BASE_DIR / "data" / "agent-stats.json"
@@ -858,6 +886,17 @@ def run_skill(name: str, req: Optional[SkillRunRequest] = None):
)
write_file(path / "learnings.md", existing + new_entry)
+ # Also push this learning into the centralized brain index so it is
+ # immediately searchable alongside brain/ notes and chat history.
+ record_brain_learning(
+ source="skill-learning",
+ source_path=str(path.relative_to(BASE_DIR) / "learnings.md") + f"#{run_id}",
+ title=f"{name} β learning {run_id}",
+ content=f"## {timestamp} (Run {run_id})\n- Agent: {agent_choice}\n- Input: {skill_input or '(none)'}\n- Output: {response_text[:500]}",
+ agent=agent_choice,
+ updated_at=get_timestamp(),
+ )
+
# Log execution
append_audit({
"action": "skill_run",