Files
ATOCore/src/atocore/engineering/wiki.py

1157 lines
49 KiB
Python
Raw Normal View History

"""AtoCore Wiki — navigable HTML pages from structured data.
A lightweight wiki served directly from the AtoCore API. Every page is
generated on-demand from the database so it's always current. Source of
truth is the database the wiki is a derived view.
Routes:
/wiki Homepage with project list + search
/wiki/projects/{name} Full project overview
/wiki/entities/{id} Entity detail with relationships
/wiki/search?q=... Search entities, memories, state
"""
from __future__ import annotations
import markdown as md
from atocore.context.project_state import get_state
from atocore.engineering.service import (
get_entities,
get_entity,
get_entity_with_context,
get_relationships,
)
from atocore.memory.service import get_memories
from atocore.projects.registry import (
GLOBAL_PROJECT,
INBOX_PROJECT,
load_project_registry,
)
feat: Phase 7I + UI refresh (capture form, memory/domain/activity pages, topnav) Closes three gaps the user surfaced: (1) OpenClaw agents run blind without AtoCore context, (2) mobile/desktop chats can't be captured at all, (3) wiki UI hadn't kept up with backend capabilities. Phase 7I — OpenClaw two-way bridge - Plugin now calls /context/build on before_agent_start and prepends the context pack to event.prompt, so whatever LLM runs underneath (sonnet, opus, codex, local model) answers grounded in AtoCore knowledge. Captured prompt stays the user's original text; fail-open with a 5s timeout. Config-gated via injectContext flag. - Plugin version 0.0.0 → 0.2.0; README rewritten. UI refresh - /wiki/capture — paste-to-ingest form for Claude Desktop / web / mobile / ChatGPT / other. Goes through normal /interactions pipeline with client="claude-desktop|claude-web|claude-mobile|chatgpt|other". Fixes the rotovap/mushroom-on-phone gap. - /wiki/memories/{id} (Phase 7E) — full memory detail: content, status, confidence, refs, valid_until, domain_tags (clickable to domain pages), project link, source chunk, graduated-to-entity link, full audit trail, related-by-tag neighbors. - /wiki/domains/{tag} (Phase 7F) — cross-project view: all active memories with the given tag grouped by project, sorted by count. Case-insensitive, whitespace-tolerant. Also surfaces graduated entities carrying the tag. - /wiki/activity — autonomous-activity timeline feed. Summary chips by action (created/promoted/merged/superseded/decayed/canonicalized) and by actor (auto-dedup-tier1, auto-dedup-tier2, confidence-decay, phase10-auto-promote, transient-to-durable, tag-canon, human-triage). Answers "what has the brain been doing while I was away?" - Home refresh: persistent topnav (Home · Activity · Capture · Triage · Dashboard), "What the brain is doing" snippet above project cards showing recent autonomous-actor counts, link to full activity. Tests: +10 (capture page, memory detail + 404, domain cross-project + empty + tag normalization, activity feed + groupings, home topnav, superseded-source detail after merge). 440 → 450. Known next: capture-browser extension for Claude.ai web (bigger project, deferred); voice/mobile relay (adjacent). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 10:14:15 -04:00
_TOP_NAV_LINKS = [
("🏠 Home", "/wiki"),
("📡 Activity", "/wiki/activity"),
("🔀 Triage", "/admin/triage"),
("📊 Dashboard", "/admin/dashboard"),
]
def _render_topnav(active_path: str = "") -> str:
items = []
for label, href in _TOP_NAV_LINKS:
cls = "topnav-item active" if href == active_path else "topnav-item"
items.append(f'<a href="{href}" class="{cls}">{label}</a>')
return f'<nav class="topnav">{" ".join(items)}</nav>'
def render_html(title: str, body_html: str, breadcrumbs: list[tuple[str, str]] | None = None, active_path: str = "") -> str:
topnav = _render_topnav(active_path)
crumbs = ""
if breadcrumbs:
parts = []
for label, href in breadcrumbs:
if href:
parts.append(f'<a href="{href}">{label}</a>')
else:
parts.append(f"<span>{label}</span>")
feat: Phase 7I + UI refresh (capture form, memory/domain/activity pages, topnav) Closes three gaps the user surfaced: (1) OpenClaw agents run blind without AtoCore context, (2) mobile/desktop chats can't be captured at all, (3) wiki UI hadn't kept up with backend capabilities. Phase 7I — OpenClaw two-way bridge - Plugin now calls /context/build on before_agent_start and prepends the context pack to event.prompt, so whatever LLM runs underneath (sonnet, opus, codex, local model) answers grounded in AtoCore knowledge. Captured prompt stays the user's original text; fail-open with a 5s timeout. Config-gated via injectContext flag. - Plugin version 0.0.0 → 0.2.0; README rewritten. UI refresh - /wiki/capture — paste-to-ingest form for Claude Desktop / web / mobile / ChatGPT / other. Goes through normal /interactions pipeline with client="claude-desktop|claude-web|claude-mobile|chatgpt|other". Fixes the rotovap/mushroom-on-phone gap. - /wiki/memories/{id} (Phase 7E) — full memory detail: content, status, confidence, refs, valid_until, domain_tags (clickable to domain pages), project link, source chunk, graduated-to-entity link, full audit trail, related-by-tag neighbors. - /wiki/domains/{tag} (Phase 7F) — cross-project view: all active memories with the given tag grouped by project, sorted by count. Case-insensitive, whitespace-tolerant. Also surfaces graduated entities carrying the tag. - /wiki/activity — autonomous-activity timeline feed. Summary chips by action (created/promoted/merged/superseded/decayed/canonicalized) and by actor (auto-dedup-tier1, auto-dedup-tier2, confidence-decay, phase10-auto-promote, transient-to-durable, tag-canon, human-triage). Answers "what has the brain been doing while I was away?" - Home refresh: persistent topnav (Home · Activity · Capture · Triage · Dashboard), "What the brain is doing" snippet above project cards showing recent autonomous-actor counts, link to full activity. Tests: +10 (capture page, memory detail + 404, domain cross-project + empty + tag normalization, activity feed + groupings, home topnav, superseded-source detail after merge). 440 → 450. Known next: capture-browser extension for Claude.ai web (bigger project, deferred); voice/mobile relay (adjacent). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 10:14:15 -04:00
crumbs = f'<nav class="breadcrumbs">{" / ".join(parts)}</nav>'
feat: Phase 7I + UI refresh (capture form, memory/domain/activity pages, topnav) Closes three gaps the user surfaced: (1) OpenClaw agents run blind without AtoCore context, (2) mobile/desktop chats can't be captured at all, (3) wiki UI hadn't kept up with backend capabilities. Phase 7I — OpenClaw two-way bridge - Plugin now calls /context/build on before_agent_start and prepends the context pack to event.prompt, so whatever LLM runs underneath (sonnet, opus, codex, local model) answers grounded in AtoCore knowledge. Captured prompt stays the user's original text; fail-open with a 5s timeout. Config-gated via injectContext flag. - Plugin version 0.0.0 → 0.2.0; README rewritten. UI refresh - /wiki/capture — paste-to-ingest form for Claude Desktop / web / mobile / ChatGPT / other. Goes through normal /interactions pipeline with client="claude-desktop|claude-web|claude-mobile|chatgpt|other". Fixes the rotovap/mushroom-on-phone gap. - /wiki/memories/{id} (Phase 7E) — full memory detail: content, status, confidence, refs, valid_until, domain_tags (clickable to domain pages), project link, source chunk, graduated-to-entity link, full audit trail, related-by-tag neighbors. - /wiki/domains/{tag} (Phase 7F) — cross-project view: all active memories with the given tag grouped by project, sorted by count. Case-insensitive, whitespace-tolerant. Also surfaces graduated entities carrying the tag. - /wiki/activity — autonomous-activity timeline feed. Summary chips by action (created/promoted/merged/superseded/decayed/canonicalized) and by actor (auto-dedup-tier1, auto-dedup-tier2, confidence-decay, phase10-auto-promote, transient-to-durable, tag-canon, human-triage). Answers "what has the brain been doing while I was away?" - Home refresh: persistent topnav (Home · Activity · Capture · Triage · Dashboard), "What the brain is doing" snippet above project cards showing recent autonomous-actor counts, link to full activity. Tests: +10 (capture page, memory detail + 404, domain cross-project + empty + tag normalization, activity feed + groupings, home topnav, superseded-source detail after merge). 440 → 450. Known next: capture-browser extension for Claude.ai web (bigger project, deferred); voice/mobile relay (adjacent). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 10:14:15 -04:00
nav = topnav + crumbs
return _TEMPLATE.replace("{{title}}", title).replace("{{nav}}", nav).replace("{{body}}", body_html)
def render_homepage() -> str:
projects = []
try:
registered = load_project_registry()
for p in registered:
entity_count = len(get_entities(project=p.project_id, limit=200))
memory_count = len(get_memories(project=p.project_id, active_only=True, limit=200))
state_entries = get_state(p.project_id)
# Pull stage/type/client from state entries
stage = ""
proj_type = ""
client = ""
for e in state_entries:
if e.category == "status":
if e.key == "stage":
stage = e.value
elif e.key == "type":
proj_type = e.value
elif e.key == "client":
client = e.value
projects.append({
"id": p.project_id,
"description": p.description,
"entities": entity_count,
"memories": memory_count,
"state": len(state_entries),
"stage": stage,
"type": proj_type,
"client": client,
})
except Exception:
pass
# Group by high-level bucket
buckets: dict[str, list] = {
"Active Contracts": [],
"Leads & Prospects": [],
"Internal Tools & Infra": [],
"Other": [],
}
for p in projects:
t = p["type"].lower()
s = p["stage"].lower()
if "lead" in t or "lead" in s or "prospect" in s:
buckets["Leads & Prospects"].append(p)
elif "contract" in t or ("active" in s and "contract" in s):
buckets["Active Contracts"].append(p)
elif "infra" in t or "tool" in t or "internal" in t:
buckets["Internal Tools & Infra"].append(p)
else:
buckets["Other"].append(p)
lines = ['<h1>AtoCore Wiki</h1>']
lines.append('<form class="search-box" action="/wiki/search" method="get">')
lines.append('<input type="text" name="q" placeholder="Search entities, memories, projects..." autofocus>')
lines.append('<button type="submit">Search</button>')
lines.append('</form>')
feat: Phase 7I + UI refresh (capture form, memory/domain/activity pages, topnav) Closes three gaps the user surfaced: (1) OpenClaw agents run blind without AtoCore context, (2) mobile/desktop chats can't be captured at all, (3) wiki UI hadn't kept up with backend capabilities. Phase 7I — OpenClaw two-way bridge - Plugin now calls /context/build on before_agent_start and prepends the context pack to event.prompt, so whatever LLM runs underneath (sonnet, opus, codex, local model) answers grounded in AtoCore knowledge. Captured prompt stays the user's original text; fail-open with a 5s timeout. Config-gated via injectContext flag. - Plugin version 0.0.0 → 0.2.0; README rewritten. UI refresh - /wiki/capture — paste-to-ingest form for Claude Desktop / web / mobile / ChatGPT / other. Goes through normal /interactions pipeline with client="claude-desktop|claude-web|claude-mobile|chatgpt|other". Fixes the rotovap/mushroom-on-phone gap. - /wiki/memories/{id} (Phase 7E) — full memory detail: content, status, confidence, refs, valid_until, domain_tags (clickable to domain pages), project link, source chunk, graduated-to-entity link, full audit trail, related-by-tag neighbors. - /wiki/domains/{tag} (Phase 7F) — cross-project view: all active memories with the given tag grouped by project, sorted by count. Case-insensitive, whitespace-tolerant. Also surfaces graduated entities carrying the tag. - /wiki/activity — autonomous-activity timeline feed. Summary chips by action (created/promoted/merged/superseded/decayed/canonicalized) and by actor (auto-dedup-tier1, auto-dedup-tier2, confidence-decay, phase10-auto-promote, transient-to-durable, tag-canon, human-triage). Answers "what has the brain been doing while I was away?" - Home refresh: persistent topnav (Home · Activity · Capture · Triage · Dashboard), "What the brain is doing" snippet above project cards showing recent autonomous-actor counts, link to full activity. Tests: +10 (capture page, memory detail + 404, domain cross-project + empty + tag normalization, activity feed + groupings, home topnav, superseded-source detail after merge). 440 → 450. Known next: capture-browser extension for Claude.ai web (bigger project, deferred); voice/mobile relay (adjacent). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 10:14:15 -04:00
# What's happening — autonomous activity snippet
try:
from atocore.memory.service import get_recent_audit
recent = get_recent_audit(limit=30)
by_action: dict[str, int] = {}
by_actor: dict[str, int] = {}
for a in recent:
by_action[a["action"]] = by_action.get(a["action"], 0) + 1
by_actor[a["actor"]] = by_actor.get(a["actor"], 0) + 1
# Surface autonomous actors specifically
auto_actors = {k: v for k, v in by_actor.items()
if k.startswith("auto-") or k == "confidence-decay"
or k == "phase10-auto-promote" or k == "transient-to-durable"}
if recent:
lines.append('<div class="activity-snippet">')
lines.append('<h3>📡 What the brain is doing</h3>')
top_actions = sorted(by_action.items(), key=lambda x: -x[1])[:6]
lines.append('<div class="stat-row">' +
"".join(f'<span>{a}: {n}</span>' for a, n in top_actions) +
'</div>')
if auto_actors:
lines.append(f'<p style="font-size:0.9rem; margin:0.3rem 0;">Autonomous actors: ' +
" · ".join(f'<code>{k}</code> ({v})' for k, v in auto_actors.items()) +
'</p>')
lines.append('<p style="font-size:0.85rem; margin:0;"><a href="/wiki/activity">Full timeline →</a></p>')
lines.append('</div>')
except Exception:
pass
# Issue C: Inbox + Global pseudo-projects alongside registered projects.
# scope_only=True keeps real-project entities out of these counts.
try:
inbox_count = len(get_entities(
project=INBOX_PROJECT, scope_only=True, limit=500,
))
global_count = len(get_entities(
project=GLOBAL_PROJECT, scope_only=True, limit=500,
))
except Exception:
inbox_count = 0
global_count = 0
lines.append('<h2>📥 Inbox &amp; Global</h2>')
lines.append(
'<p class="emerging-intro">Entities that don\'t belong to a specific '
'project yet. <strong>Inbox</strong> holds pre-project leads and quotes. '
'<strong>Global</strong> holds cross-project facts (material properties, '
'vendor capabilities) that apply everywhere.</p>'
)
lines.append('<div class="card-grid">')
lines.append(
f'<a href="/entities?project=inbox&scope_only=true" class="card">'
f'<h3>📥 Inbox</h3>'
f'<p>Pre-project leads, quotes, early conversations.</p>'
f'<div class="stats">{inbox_count} entities</div>'
f'</a>'
)
lines.append(
f'<a href="/entities?project=&scope_only=true" class="card">'
f'<h3>🌐 Global</h3>'
f'<p>Cross-project facts: materials, vendors, shared knowledge.</p>'
f'<div class="stats">{global_count} entities</div>'
f'</a>'
)
lines.append('</div>')
for bucket_name, items in buckets.items():
if not items:
continue
lines.append(f'<h2>{bucket_name}</h2>')
lines.append('<div class="card-grid">')
for p in items:
client_line = f'<div class="client">{p["client"]}</div>' if p["client"] else ''
stage_tag = f'<span class="tag">{p["stage"].split("")[0]}</span>' if p["stage"] else ''
lines.append(f'<a href="/wiki/projects/{p["id"]}" class="card">')
lines.append(f'<h3>{p["id"]} {stage_tag}</h3>')
lines.append(client_line)
lines.append(f'<p>{p["description"][:140]}</p>')
lines.append(f'<div class="stats">{p["entities"]} entities · {p["memories"]} memories · {p["state"]} state</div>')
lines.append('</a>')
lines.append('</div>')
feat: Phase 6 — Living Taxonomy + Universal Capture Closes two real-use gaps: 1. "APM tool" gap: work done outside Claude Code (desktop, web, phone, other machine) was invisible to AtoCore. 2. Project discovery gap: manual JSON-file edits required to promote an emerging theme to a first-class project. B — atocore_remember MCP tool (scripts/atocore_mcp.py): - New MCP tool for universal capture from any MCP-aware client (Claude Desktop, Code, Cursor, Zed, Windsurf, etc.) - Accepts content (required) + memory_type/project/confidence/ valid_until/domain_tags (all optional with sensible defaults) - Creates a candidate memory, goes through the existing 3-tier triage (no bypass — the quality gate catches noise) - Detailed tool description guides Claude on when to invoke: "remember this", "save that for later", "don't lose this fact" - Total tools exposed by MCP server: 14 → 15 C.1 Emerging-concepts detector (scripts/detect_emerging.py): - Nightly scan of active + candidate memories for: * Unregistered project names with ≥3 memory occurrences * Top 20 domain_tags by frequency (emerging categories) * Active memories with reference_count ≥ 5 + valid_until set (reinforced transients — candidates for extension) - Writes findings to atocore/proposals/* project state entries - Emits "warning" alert via Phase 4 framework the FIRST time a new project crosses the 5-memory alert threshold (avoids spam) - Configurable via env vars: ATOCORE_EMERGING_PROJECT_MIN (default 3), ATOCORE_EMERGING_ALERT_THRESHOLD (default 5), TOP_TAGS_LIMIT (20) C.2 Registration surface (src/atocore/api/routes.py + wiki.py): - POST /admin/projects/register-emerging — one-click register with sensible defaults (ingest_roots auto-filled with vault:incoming/projects/<id>/ convention). Clears the proposal from the dashboard list on success. - Dashboard /admin/dashboard: new "proposals" section with unregistered_projects + emerging_categories + reinforced_transients. - Wiki homepage: "📋 Emerging" section rendering each unregistered project as a card with count + 2 sample memory previews + inline "📌 Register as project" button that calls the endpoint via fetch, reloads the page on success. C.3 Transient-to-durable extension (src/atocore/memory/service.py + API + cron): - New extend_reinforced_valid_until() function — scans active memories with valid_until in the next 30 days and reference_count ≥ 5. Extends expiry by 90 days. If reference_count ≥ 10, clears expiry entirely (makes permanent). Writes audit rows via the Phase 4 memory_audit framework with actor="transient-to-durable". - POST /admin/memory/extend-reinforced — API wrapper for cron. - Matches the user's intuition: "something transient becomes important if you keep coming back to it". Nightly cron (deploy/dalidou/batch-extract.sh): - Step F2: detect_emerging.py (after F pipeline summary) - Step F3: /admin/memory/extend-reinforced (before integrity check) - Both fail-open; errors don't break the pipeline. Tests: 366 → 374 (+8 for Phase 6): - 6 tests for extend_reinforced_valid_until covering: extension path, permanent path, skip far-future, skip low-refs, skip permanent memories, audit row write - 2 smoke tests for the detector (imports cleanly, handles empty DB) - MCP tool changes don't need new tests — the wrapper is pure passthrough Design decisions documented in plan file: - atocore_remember deliberately doesn't bypass triage (quality gate) - Detector is passive (surfaces proposals) not active (auto-registers) - Sensible ingest-root defaults ("vault:incoming/projects/<id>/") so registration is one-click with no file-path thinking - Extension adds 90 days rather than clearing expiry (gradual permanence earned through sustained reinforcement) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 08:08:55 -04:00
# Phase 6 C.2: Emerging projects section
try:
import json as _json
emerging_projects = []
state_entries = get_state("atocore")
for e in state_entries:
if e.category == "proposals" and e.key == "unregistered_projects":
try:
emerging_projects = _json.loads(e.value)
except Exception:
emerging_projects = []
break
if emerging_projects:
lines.append('<h2>📋 Emerging</h2>')
lines.append('<p class="emerging-intro">Projects that appear in memories but aren\'t yet registered. '
'One click to promote them to first-class projects.</p>')
lines.append('<div class="emerging-grid">')
for ep in emerging_projects[:10]:
name = ep.get("project", "?")
count = ep.get("count", 0)
samples = ep.get("sample_contents", [])
samples_html = "".join(f'<li>{s[:120]}</li>' for s in samples[:2])
lines.append(
f'<div class="emerging-card">'
f'<h3>{name}</h3>'
f'<div class="emerging-count">{count} memories</div>'
f'<ul class="emerging-samples">{samples_html}</ul>'
f'<button class="btn-register-emerging" onclick="registerEmerging({name!r})">📌 Register as project</button>'
f'</div>'
)
lines.append('</div>')
except Exception:
pass
# Quick stats
all_entities = get_entities(limit=500)
all_memories = get_memories(active_only=True, limit=500)
pending = get_memories(status="candidate", limit=500)
lines.append('<h2>System</h2>')
lines.append(f'<p>{len(all_entities)} entities · {len(all_memories)} active memories · {len(projects)} projects</p>')
# Triage queue prompt — surfaced prominently if non-empty
if pending:
tone = "triage-warning" if len(pending) > 50 else "triage-notice"
lines.append(
f'<p class="{tone}">🗂️ <strong>{len(pending)} candidates</strong> awaiting triage — '
f'<a href="/admin/triage">review now →</a></p>'
)
lines.append(f'<p><a href="/admin/triage">Triage Queue</a> · <a href="/admin/dashboard">API Dashboard (JSON)</a> · <a href="/health">Health Check</a></p>')
feat: Phase 7I + UI refresh (capture form, memory/domain/activity pages, topnav) Closes three gaps the user surfaced: (1) OpenClaw agents run blind without AtoCore context, (2) mobile/desktop chats can't be captured at all, (3) wiki UI hadn't kept up with backend capabilities. Phase 7I — OpenClaw two-way bridge - Plugin now calls /context/build on before_agent_start and prepends the context pack to event.prompt, so whatever LLM runs underneath (sonnet, opus, codex, local model) answers grounded in AtoCore knowledge. Captured prompt stays the user's original text; fail-open with a 5s timeout. Config-gated via injectContext flag. - Plugin version 0.0.0 → 0.2.0; README rewritten. UI refresh - /wiki/capture — paste-to-ingest form for Claude Desktop / web / mobile / ChatGPT / other. Goes through normal /interactions pipeline with client="claude-desktop|claude-web|claude-mobile|chatgpt|other". Fixes the rotovap/mushroom-on-phone gap. - /wiki/memories/{id} (Phase 7E) — full memory detail: content, status, confidence, refs, valid_until, domain_tags (clickable to domain pages), project link, source chunk, graduated-to-entity link, full audit trail, related-by-tag neighbors. - /wiki/domains/{tag} (Phase 7F) — cross-project view: all active memories with the given tag grouped by project, sorted by count. Case-insensitive, whitespace-tolerant. Also surfaces graduated entities carrying the tag. - /wiki/activity — autonomous-activity timeline feed. Summary chips by action (created/promoted/merged/superseded/decayed/canonicalized) and by actor (auto-dedup-tier1, auto-dedup-tier2, confidence-decay, phase10-auto-promote, transient-to-durable, tag-canon, human-triage). Answers "what has the brain been doing while I was away?" - Home refresh: persistent topnav (Home · Activity · Capture · Triage · Dashboard), "What the brain is doing" snippet above project cards showing recent autonomous-actor counts, link to full activity. Tests: +10 (capture page, memory detail + 404, domain cross-project + empty + tag normalization, activity feed + groupings, home topnav, superseded-source detail after merge). 440 → 450. Known next: capture-browser extension for Claude.ai web (bigger project, deferred); voice/mobile relay (adjacent). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 10:14:15 -04:00
return render_html("AtoCore Wiki", "\n".join(lines), active_path="/wiki")
def render_project(project: str) -> str:
from atocore.engineering.mirror import generate_project_overview
markdown_content = generate_project_overview(project)
# Resolve [[Wikilinks]] before markdown so redlinks / cross-project
# indicators appear in the rendered HTML. (Issue B)
markdown_content = _wikilink_transform(markdown_content, current_project=project)
# Convert entity names to links
entities = get_entities(project=project, limit=200)
html_body = md.markdown(markdown_content, extensions=["tables", "fenced_code"])
for ent in sorted(entities, key=lambda e: len(e.name), reverse=True):
linked = f'<a href="/wiki/entities/{ent.id}" title="{ent.entity_type}">{ent.name}</a>'
html_body = html_body.replace(f"<strong>{ent.name}</strong>", f"<strong>{linked}</strong>", 1)
return render_html(
f"{project}",
html_body,
breadcrumbs=[("Wiki", "/wiki"), (project, "")],
)
import re as _re
_WIKILINK_PATTERN = _re.compile(r"\[\[([^\[\]|]+?)(?:\|([^\[\]]+?))?\]\]")
def _resolve_wikilink(target: str, current_project: str | None) -> tuple[str, str, str]:
"""Resolve a ``[[Name]]`` target to ``(href, css_class, extra_suffix)``.
Resolution order (Issue B):
1. Same-project exact name match live link (class ``wikilink``).
2. Other-project exact name match live link with ``(in project X)``
suffix (class ``wikilink wikilink-cross``).
3. No match redlink pointing at ``/wiki/new?name=...`` so clicking
opens a pre-filled "create this entity" form (class ``redlink``).
"""
needle = target.strip()
if not needle:
return ("/wiki/new", "redlink", "")
same_project = None
cross_project = None
try:
candidates = get_entities(name_contains=needle, limit=200)
except Exception:
candidates = []
lowered = needle.lower()
for ent in candidates:
if ent.name.lower() != lowered:
continue
if current_project and ent.project == current_project:
same_project = ent
break
if cross_project is None:
cross_project = ent
if same_project is not None:
return (f"/wiki/entities/{same_project.id}", "wikilink", "")
if cross_project is not None:
suffix = (
f' <span class="wikilink-scope">(in {cross_project.project})</span>'
if cross_project.project
else ' <span class="wikilink-scope">(global)</span>'
)
return (f"/wiki/entities/{cross_project.id}", "wikilink wikilink-cross", suffix)
from urllib.parse import quote
href = f"/wiki/new?name={quote(needle)}"
if current_project:
href += f"&project={quote(current_project)}"
return (href, "redlink", "")
def _wikilink_transform(text: str, current_project: str | None) -> str:
"""Replace ``[[Name]]`` / ``[[Name|Display]]`` tokens with HTML anchors.
Runs before markdown rendering. Emits raw HTML which python-markdown
preserves unchanged.
"""
if not text or "[[" not in text:
return text
def _sub(match: _re.Match) -> str:
target = match.group(1)
display = (match.group(2) or target).strip()
href, cls, suffix = _resolve_wikilink(target, current_project)
title = "create this entity" if cls == "redlink" else target.strip()
return (
f'<a href="{href}" class="{cls}" title="{_escape_attr(title)}">'
f'{_escape_html(display)}</a>{suffix}'
)
return _WIKILINK_PATTERN.sub(_sub, text)
def render_new_entity_form(name: str = "", project: str = "") -> str:
"""Issue B — "create this entity" form targeted by redlinks."""
from atocore.engineering.service import ENTITY_TYPES
safe_name = _escape_attr(name or "")
safe_project = _escape_attr(project or "")
opts = "".join(
f'<option value="{t}">{t}</option>' for t in ENTITY_TYPES
)
lines = [
'<h1>Create entity</h1>',
('<p>This entity was referenced via a wikilink but does not exist yet. '
'Fill in the details to create it — the wiki link will resolve on reload.</p>'),
'<form id="new-entity-form" class="new-entity-form">',
f'<label>Name<br><input type="text" name="name" value="{safe_name}" required></label>',
f'<label>Entity type<br><select name="entity_type" required>{opts}</select></label>',
f'<label>Project<br><input type="text" name="project" value="{safe_project}" '
f'placeholder="leave blank for cross-project / global"></label>',
'<label>Description<br><textarea name="description" rows="4"></textarea></label>',
'<button type="submit">Create</button>',
'<div id="new-entity-result" class="new-entity-result"></div>',
'</form>',
"""<script>
(function() {
const form = document.getElementById('new-entity-form');
const out = document.getElementById('new-entity-result');
form.addEventListener('submit', async (ev) => {
ev.preventDefault();
const fd = new FormData(form);
const body = {
name: fd.get('name'),
entity_type: fd.get('entity_type'),
project: fd.get('project') || '',
description: fd.get('description') || '',
};
try {
const r = await fetch('/v1/entities', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(body),
});
const j = await r.json();
if (r.ok) {
out.innerHTML = 'Created: <a href="/wiki/entities/' + j.id + '">' +
(j.name || 'new entity') + '</a>';
setTimeout(() => { window.location.href = '/wiki/entities/' + j.id; }, 800);
} else {
out.textContent = 'Error: ' + (j.detail || JSON.stringify(j));
}
} catch (e) {
out.textContent = 'Network error: ' + e;
}
});
})();
</script>""",
]
return render_html(
f"Create {name}" if name else "Create entity",
"\n".join(lines),
breadcrumbs=[("Wiki", "/wiki"), ("Create entity", "")],
)
feat(assets): binary asset store + artifact entity + wiki evidence (Issue F) Wires visual evidence into the knowledge graph. Images, PDFs, and CAD exports can now be uploaded, deduped by SHA-256, thumbnailed, linked to entities via EVIDENCED_BY, and rendered inline on wiki pages. Unblocks AKC uploading voice-session screenshots alongside extracted entities. - assets/ module: store_asset (hash dedup + MIME allowlist + 20 MB cap), get_asset_binary, get_thumbnail (Pillow, on-disk cache under .thumbnails/<size>/), list_orphan_assets, invalidate_asset - models/database.py: new `assets` table + indexes - engineering/service.py: `artifact` added to ENTITY_TYPES - api/routes.py: POST /assets (multipart), GET /assets/{id}, /assets/{id}/thumbnail, /assets/{id}/meta, /admin/assets/orphans, DELETE /assets/{id} (409 if still referenced), GET /entities/{id}/evidence (EVIDENCED_BY artifacts with asset meta) - main.py: all new paths aliased under /v1 - engineering/wiki.py: entity pages render EVIDENCED_BY → artifact as a "Visual evidence" thumbnail strip; artifact pages render the full image + caption + capture_context - deploy/dalidou/docker-compose.yml: bind-mount ${ATOCORE_ASSETS_DIR} - config.py: assets_dir + assets_max_upload_bytes settings - requirements.txt + pyproject.toml: python-multipart, Pillow>=10.0.0 - tests/test_assets.py: 16 tests (dedup, cap, thumbnail cache, orphan detection, invalidate gating, API upload/fetch, evidence, v1 aliases, wiki rendering) - DEV-LEDGER.md: session log + cleanup note + test_count 478 -> 494 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 21:46:52 -04:00
def _render_visual_evidence(entity_id: str, ctx: dict) -> str:
"""Render EVIDENCED_BY → artifact links as an inline thumbnail strip."""
from atocore.assets import get_asset
artifacts = []
for rel in ctx["relationships"]:
if rel.source_entity_id != entity_id or rel.relationship_type != "evidenced_by":
continue
target = ctx["related_entities"].get(rel.target_entity_id)
if target is None or target.entity_type != "artifact":
continue
artifacts.append(target)
if not artifacts:
return ""
tiles = []
for art in artifacts:
props = art.properties or {}
kind = props.get("kind", "other")
caption = props.get("caption", art.name)
asset_id = props.get("asset_id")
asset = get_asset(asset_id) if asset_id else None
detail_href = f"/wiki/entities/{art.id}"
if kind == "image" and asset and asset.mime_type.startswith("image/"):
full_href = f"/assets/{asset.id}"
thumb = f"/assets/{asset.id}/thumbnail?size=240"
tiles.append(
f'<figure class="evidence-tile">'
f'<a href="{full_href}" target="_blank" rel="noopener">'
f'<img src="{thumb}" alt="{_escape_attr(caption)}" loading="lazy">'
f'</a>'
f'<figcaption><a href="{detail_href}">{_escape_html(caption)}</a></figcaption>'
f'</figure>'
)
elif kind == "pdf" and asset:
full_href = f"/assets/{asset.id}"
tiles.append(
f'<div class="evidence-tile evidence-pdf">'
f'<a href="{full_href}" target="_blank" rel="noopener">'
f'📄 PDF: {_escape_html(caption)}</a>'
f' · <a href="{detail_href}">details</a>'
f'</div>'
)
else:
tiles.append(
f'<div class="evidence-tile evidence-other">'
f'<a href="{detail_href}">📎 {_escape_html(caption)}</a>'
f' <span class="tag">{kind}</span>'
f'</div>'
)
return (
'<h2>Visual evidence</h2>'
f'<div class="evidence-strip">{"".join(tiles)}</div>'
)
def _render_artifact_body(ent) -> list[str]:
"""Render an artifact entity's own image/pdf/caption."""
from atocore.assets import get_asset
props = ent.properties or {}
kind = props.get("kind", "other")
caption = props.get("caption", "")
capture_context = props.get("capture_context", "")
asset_id = props.get("asset_id")
asset = get_asset(asset_id) if asset_id else None
out: list[str] = []
if kind == "image" and asset and asset.mime_type.startswith("image/"):
out.append(
f'<figure class="artifact-full">'
f'<a href="/assets/{asset.id}" target="_blank" rel="noopener">'
f'<img src="/assets/{asset.id}/thumbnail?size=1024" '
f'alt="{_escape_attr(caption or ent.name)}">'
f'</a>'
f'<figcaption>{_escape_html(caption)}</figcaption>'
f'</figure>'
)
elif kind == "pdf" and asset:
out.append(
f'<p>📄 <a href="/assets/{asset.id}" target="_blank" rel="noopener">'
f'Open PDF ({asset.size_bytes // 1024} KB)</a></p>'
)
elif asset_id:
out.append(f'<p class="meta">asset_id: <code>{asset_id}</code> — blob missing</p>')
if capture_context:
out.append('<h2>Capture context</h2>')
out.append(f'<blockquote>{_escape_html(capture_context)}</blockquote>')
return out
def _escape_html(s: str) -> str:
if s is None:
return ""
return (str(s)
.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;"))
def _escape_attr(s: str) -> str:
return _escape_html(s).replace('"', "&quot;")
def render_entity(entity_id: str) -> str | None:
ctx = get_entity_with_context(entity_id)
if ctx is None:
return None
ent = ctx["entity"]
lines = [f'<h1>[{ent.entity_type}] {ent.name}</h1>']
if ent.project:
lines.append(f'<p>Project: <a href="/wiki/projects/{ent.project}">{ent.project}</a></p>')
if ent.description:
desc_html = _wikilink_transform(ent.description, current_project=ent.project)
lines.append(f'<p>{desc_html}</p>')
if ent.properties:
lines.append('<h2>Properties</h2><ul>')
for k, v in ent.properties.items():
lines.append(f'<li><strong>{k}</strong>: {v}</li>')
lines.append('</ul>')
lines.append(f'<p class="meta">confidence: {ent.confidence} · status: {ent.status} · created: {ent.created_at}</p>')
feat(assets): binary asset store + artifact entity + wiki evidence (Issue F) Wires visual evidence into the knowledge graph. Images, PDFs, and CAD exports can now be uploaded, deduped by SHA-256, thumbnailed, linked to entities via EVIDENCED_BY, and rendered inline on wiki pages. Unblocks AKC uploading voice-session screenshots alongside extracted entities. - assets/ module: store_asset (hash dedup + MIME allowlist + 20 MB cap), get_asset_binary, get_thumbnail (Pillow, on-disk cache under .thumbnails/<size>/), list_orphan_assets, invalidate_asset - models/database.py: new `assets` table + indexes - engineering/service.py: `artifact` added to ENTITY_TYPES - api/routes.py: POST /assets (multipart), GET /assets/{id}, /assets/{id}/thumbnail, /assets/{id}/meta, /admin/assets/orphans, DELETE /assets/{id} (409 if still referenced), GET /entities/{id}/evidence (EVIDENCED_BY artifacts with asset meta) - main.py: all new paths aliased under /v1 - engineering/wiki.py: entity pages render EVIDENCED_BY → artifact as a "Visual evidence" thumbnail strip; artifact pages render the full image + caption + capture_context - deploy/dalidou/docker-compose.yml: bind-mount ${ATOCORE_ASSETS_DIR} - config.py: assets_dir + assets_max_upload_bytes settings - requirements.txt + pyproject.toml: python-multipart, Pillow>=10.0.0 - tests/test_assets.py: 16 tests (dedup, cap, thumbnail cache, orphan detection, invalidate gating, API upload/fetch, evidence, v1 aliases, wiki rendering) - DEV-LEDGER.md: session log + cleanup note + test_count 478 -> 494 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 21:46:52 -04:00
# Issue F: artifact entities render their own image inline; other
# entities render their EVIDENCED_BY artifacts as a visual strip.
if ent.entity_type == "artifact":
lines.extend(_render_artifact_body(ent))
else:
evidence_html = _render_visual_evidence(ent.id, ctx)
if evidence_html:
lines.append(evidence_html)
if ctx["relationships"]:
lines.append('<h2>Relationships</h2><ul>')
for rel in ctx["relationships"]:
other_id = rel.target_entity_id if rel.source_entity_id == entity_id else rel.source_entity_id
other = ctx["related_entities"].get(other_id)
if other:
direction = "\u2192" if rel.source_entity_id == entity_id else "\u2190"
lines.append(
f'<li>{direction} <em>{rel.relationship_type}</em> '
f'<a href="/wiki/entities/{other_id}">[{other.entity_type}] {other.name}</a></li>'
)
lines.append('</ul>')
breadcrumbs = [("Wiki", "/wiki")]
if ent.project:
breadcrumbs.append((ent.project, f"/wiki/projects/{ent.project}"))
breadcrumbs.append((ent.name, ""))
return render_html(ent.name, "\n".join(lines), breadcrumbs=breadcrumbs)
def render_search(query: str) -> str:
lines = [f'<h1>Search: "{query}"</h1>']
# Search entities by name
entities = get_entities(name_contains=query, limit=20)
if entities:
lines.append(f'<h2>Entities ({len(entities)})</h2><ul>')
for e in entities:
proj = f' <span class="tag">{e.project}</span>' if e.project else ''
lines.append(
f'<li><a href="/wiki/entities/{e.id}">[{e.entity_type}] {e.name}</a>{proj}'
f'{"" + e.description[:100] if e.description else ""}</li>'
)
lines.append('</ul>')
feat: Phase 3 V1 — Auto-Organization (domain_tags + valid_until) Adds structural metadata that the LLM triage was already implicitly reasoning about ("stale snapshot" → reject). Phase 3 captures that reasoning as fields so it can DRIVE retrieval, not just rejection. Schema (src/atocore/models/database.py): - domain_tags TEXT DEFAULT '[]' JSON array of lowercase topic keywords - valid_until DATETIME ISO date; null = permanent - idx_memories_valid_until index for efficient expiry queries Memory service (src/atocore/memory/service.py): - Memory dataclass gains domain_tags + valid_until - create_memory, update_memory accept/persist both - _row_to_memory safely reads both (JSON-decode + null handling) - _normalize_tags helper: lowercase, dedup, strip, cap at 10 - get_memories_for_context filters expired (valid_until < today UTC) - _rank_memories_for_query adds tag-boost: memories whose domain_tags appear as substrings in query text rank higher (tertiary key after content-overlap density + absolute overlap, before confidence) LLM extractor (_llm_prompt.py → llm-0.5.0): - SYSTEM_PROMPT documents domain_tags (2-5 keywords) + valid_until (time-bounded facts get expiry dates; durable facts stay null) - normalize_candidate_item parses both fields from model output with graceful fallback for string/null/missing LLM triage (scripts/auto_triage.py): - TRIAGE_SYSTEM_PROMPT documents same two fields - parse_verdict extracts them from verdict JSON - On promote: PUT /memory/{id} with tags + valid_until BEFORE POST /memory/{id}/promote, so active memories carry them API (src/atocore/api/routes.py): - MemoryCreateRequest: adds domain_tags, valid_until - MemoryUpdateRequest: adds domain_tags, valid_until, memory_type - GET /memory response exposes domain_tags + valid_until + created_at Triage UI (src/atocore/engineering/triage_ui.py): - Renders existing tags as colored badges - Adds inline text field for tags (comma-separated) + date picker for valid_until on every candidate card - Save&Promote button persists edits via PUT then promotes - Plain Promote (and Y shortcut) also saves tags/expiry if edited Wiki (src/atocore/engineering/wiki.py): - Search now matches memory content OR domain_tags - Search results render tags as clickable badges linking to /wiki/search?q=<tag> for cross-project navigation - valid_until shown as amber "valid until YYYY-MM-DD" hint Tests: 303 → 308 (5 new for Phase 3 behavior): - test_create_memory_with_tags_and_valid_until - test_create_memory_normalizes_tags - test_update_memory_sets_tags_and_valid_until - test_get_memories_for_context_excludes_expired - test_context_builder_tag_boost_orders_results Deferred (explicitly): temporal_scope enum, source_refs memory graph, HDBSCAN clustering, memory detail wiki page, backfill of existing actives. See docs/MASTER-BRAIN-PLAN.md. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 21:37:01 -04:00
# Search memories — match on content OR domain_tags (Phase 3)
all_memories = get_memories(active_only=True, limit=200)
query_lower = query.lower()
feat: Phase 3 V1 — Auto-Organization (domain_tags + valid_until) Adds structural metadata that the LLM triage was already implicitly reasoning about ("stale snapshot" → reject). Phase 3 captures that reasoning as fields so it can DRIVE retrieval, not just rejection. Schema (src/atocore/models/database.py): - domain_tags TEXT DEFAULT '[]' JSON array of lowercase topic keywords - valid_until DATETIME ISO date; null = permanent - idx_memories_valid_until index for efficient expiry queries Memory service (src/atocore/memory/service.py): - Memory dataclass gains domain_tags + valid_until - create_memory, update_memory accept/persist both - _row_to_memory safely reads both (JSON-decode + null handling) - _normalize_tags helper: lowercase, dedup, strip, cap at 10 - get_memories_for_context filters expired (valid_until < today UTC) - _rank_memories_for_query adds tag-boost: memories whose domain_tags appear as substrings in query text rank higher (tertiary key after content-overlap density + absolute overlap, before confidence) LLM extractor (_llm_prompt.py → llm-0.5.0): - SYSTEM_PROMPT documents domain_tags (2-5 keywords) + valid_until (time-bounded facts get expiry dates; durable facts stay null) - normalize_candidate_item parses both fields from model output with graceful fallback for string/null/missing LLM triage (scripts/auto_triage.py): - TRIAGE_SYSTEM_PROMPT documents same two fields - parse_verdict extracts them from verdict JSON - On promote: PUT /memory/{id} with tags + valid_until BEFORE POST /memory/{id}/promote, so active memories carry them API (src/atocore/api/routes.py): - MemoryCreateRequest: adds domain_tags, valid_until - MemoryUpdateRequest: adds domain_tags, valid_until, memory_type - GET /memory response exposes domain_tags + valid_until + created_at Triage UI (src/atocore/engineering/triage_ui.py): - Renders existing tags as colored badges - Adds inline text field for tags (comma-separated) + date picker for valid_until on every candidate card - Save&Promote button persists edits via PUT then promotes - Plain Promote (and Y shortcut) also saves tags/expiry if edited Wiki (src/atocore/engineering/wiki.py): - Search now matches memory content OR domain_tags - Search results render tags as clickable badges linking to /wiki/search?q=<tag> for cross-project navigation - valid_until shown as amber "valid until YYYY-MM-DD" hint Tests: 303 → 308 (5 new for Phase 3 behavior): - test_create_memory_with_tags_and_valid_until - test_create_memory_normalizes_tags - test_update_memory_sets_tags_and_valid_until - test_get_memories_for_context_excludes_expired - test_context_builder_tag_boost_orders_results Deferred (explicitly): temporal_scope enum, source_refs memory graph, HDBSCAN clustering, memory detail wiki page, backfill of existing actives. See docs/MASTER-BRAIN-PLAN.md. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 21:37:01 -04:00
matching_mems = [
m for m in all_memories
if query_lower in m.content.lower()
or any(query_lower in (t or "").lower() for t in (m.domain_tags or []))
][:20]
if matching_mems:
lines.append(f'<h2>Memories ({len(matching_mems)})</h2><ul>')
for m in matching_mems:
proj = f' <span class="tag">{m.project}</span>' if m.project else ''
feat: Phase 3 V1 — Auto-Organization (domain_tags + valid_until) Adds structural metadata that the LLM triage was already implicitly reasoning about ("stale snapshot" → reject). Phase 3 captures that reasoning as fields so it can DRIVE retrieval, not just rejection. Schema (src/atocore/models/database.py): - domain_tags TEXT DEFAULT '[]' JSON array of lowercase topic keywords - valid_until DATETIME ISO date; null = permanent - idx_memories_valid_until index for efficient expiry queries Memory service (src/atocore/memory/service.py): - Memory dataclass gains domain_tags + valid_until - create_memory, update_memory accept/persist both - _row_to_memory safely reads both (JSON-decode + null handling) - _normalize_tags helper: lowercase, dedup, strip, cap at 10 - get_memories_for_context filters expired (valid_until < today UTC) - _rank_memories_for_query adds tag-boost: memories whose domain_tags appear as substrings in query text rank higher (tertiary key after content-overlap density + absolute overlap, before confidence) LLM extractor (_llm_prompt.py → llm-0.5.0): - SYSTEM_PROMPT documents domain_tags (2-5 keywords) + valid_until (time-bounded facts get expiry dates; durable facts stay null) - normalize_candidate_item parses both fields from model output with graceful fallback for string/null/missing LLM triage (scripts/auto_triage.py): - TRIAGE_SYSTEM_PROMPT documents same two fields - parse_verdict extracts them from verdict JSON - On promote: PUT /memory/{id} with tags + valid_until BEFORE POST /memory/{id}/promote, so active memories carry them API (src/atocore/api/routes.py): - MemoryCreateRequest: adds domain_tags, valid_until - MemoryUpdateRequest: adds domain_tags, valid_until, memory_type - GET /memory response exposes domain_tags + valid_until + created_at Triage UI (src/atocore/engineering/triage_ui.py): - Renders existing tags as colored badges - Adds inline text field for tags (comma-separated) + date picker for valid_until on every candidate card - Save&Promote button persists edits via PUT then promotes - Plain Promote (and Y shortcut) also saves tags/expiry if edited Wiki (src/atocore/engineering/wiki.py): - Search now matches memory content OR domain_tags - Search results render tags as clickable badges linking to /wiki/search?q=<tag> for cross-project navigation - valid_until shown as amber "valid until YYYY-MM-DD" hint Tests: 303 → 308 (5 new for Phase 3 behavior): - test_create_memory_with_tags_and_valid_until - test_create_memory_normalizes_tags - test_update_memory_sets_tags_and_valid_until - test_get_memories_for_context_excludes_expired - test_context_builder_tag_boost_orders_results Deferred (explicitly): temporal_scope enum, source_refs memory graph, HDBSCAN clustering, memory detail wiki page, backfill of existing actives. See docs/MASTER-BRAIN-PLAN.md. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 21:37:01 -04:00
tags_html = ""
if m.domain_tags:
tag_links = " ".join(
f'<a href="/wiki/search?q={t}" class="tag-badge">{t}</a>'
for t in m.domain_tags[:5]
)
tags_html = f' <span class="mem-tags">{tag_links}</span>'
expiry_html = ""
if m.valid_until:
expiry_html = f' <span class="mem-expiry">valid until {m.valid_until[:10]}</span>'
lines.append(
f'<li>[{m.memory_type}]{proj}{tags_html}{expiry_html} '
f'{m.content[:200]}</li>'
)
lines.append('</ul>')
if not entities and not matching_mems:
lines.append('<p>No results found.</p>')
lines.append('<form class="search-box" action="/wiki/search" method="get">')
lines.append(f'<input type="text" name="q" value="{query}" autofocus>')
lines.append('<button type="submit">Search</button>')
lines.append('</form>')
return render_html(
f"Search: {query}",
"\n".join(lines),
breadcrumbs=[("Wiki", "/wiki"), ("Search", "")],
)
feat: Phase 7I + UI refresh (capture form, memory/domain/activity pages, topnav) Closes three gaps the user surfaced: (1) OpenClaw agents run blind without AtoCore context, (2) mobile/desktop chats can't be captured at all, (3) wiki UI hadn't kept up with backend capabilities. Phase 7I — OpenClaw two-way bridge - Plugin now calls /context/build on before_agent_start and prepends the context pack to event.prompt, so whatever LLM runs underneath (sonnet, opus, codex, local model) answers grounded in AtoCore knowledge. Captured prompt stays the user's original text; fail-open with a 5s timeout. Config-gated via injectContext flag. - Plugin version 0.0.0 → 0.2.0; README rewritten. UI refresh - /wiki/capture — paste-to-ingest form for Claude Desktop / web / mobile / ChatGPT / other. Goes through normal /interactions pipeline with client="claude-desktop|claude-web|claude-mobile|chatgpt|other". Fixes the rotovap/mushroom-on-phone gap. - /wiki/memories/{id} (Phase 7E) — full memory detail: content, status, confidence, refs, valid_until, domain_tags (clickable to domain pages), project link, source chunk, graduated-to-entity link, full audit trail, related-by-tag neighbors. - /wiki/domains/{tag} (Phase 7F) — cross-project view: all active memories with the given tag grouped by project, sorted by count. Case-insensitive, whitespace-tolerant. Also surfaces graduated entities carrying the tag. - /wiki/activity — autonomous-activity timeline feed. Summary chips by action (created/promoted/merged/superseded/decayed/canonicalized) and by actor (auto-dedup-tier1, auto-dedup-tier2, confidence-decay, phase10-auto-promote, transient-to-durable, tag-canon, human-triage). Answers "what has the brain been doing while I was away?" - Home refresh: persistent topnav (Home · Activity · Capture · Triage · Dashboard), "What the brain is doing" snippet above project cards showing recent autonomous-actor counts, link to full activity. Tests: +10 (capture page, memory detail + 404, domain cross-project + empty + tag normalization, activity feed + groupings, home topnav, superseded-source detail after merge). 440 → 450. Known next: capture-browser extension for Claude.ai web (bigger project, deferred); voice/mobile relay (adjacent). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 10:14:15 -04:00
# ---------------------------------------------------------------------
feat: Claude Code context injection (UserPromptSubmit hook) Closes the asymmetry the user surfaced: before this, Claude Code captured every turn (Stop hook) but retrieval only happened when Claude chose to call atocore_context (opt-in MCP tool). OpenClaw had both sides covered after 7I; Claude Code did not. Now symmetric. Every Claude Code prompt is auto-sent to /context/build and the returned pack is prepended via hookSpecificOutput.additionalContext — same as what OpenClaw's before_agent_start hook now does. - deploy/hooks/inject_context.py — UserPromptSubmit hook. Fail-open (always exit 0). Skips short/XML prompts. 5s timeout. Project inference mirrors capture_stop.py cwd→slug table. Kill switch: ATOCORE_CONTEXT_DISABLED=1. - ~/.claude/settings.json registered the hook (local config, not committed; copy-paste snippet in docs/capture-surfaces.md). - Removed /wiki/capture from topnav. Endpoint still exists but the page is now labeled "fallback only" with a warning banner. The sanctioned surfaces are Claude Code + OpenClaw; manual paste is explicitly not the design. - docs/capture-surfaces.md — scope statement: two surfaces, nothing else. Anthropic API polling explicitly prohibited. Tests: +8 for inject_context.py (exit 0 on all failure modes, kill switch, short prompt filter, XML filter, bad stdin, mock-server success shape, project inference from cwd). Updated 2 wiki tests for the topnav change. 450 → 459. Verified live with real AtoCore: injected 2979 chars of atocore project context on a cwd-matched prompt. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 12:01:41 -04:00
# /wiki/capture — DEPRECATED emergency paste-in form.
# Kept as an endpoint because POST /interactions is public anyway, but
# REMOVED from the topnav so it's not promoted as the capture path.
# The sanctioned surfaces are Claude Code (Stop + UserPromptSubmit
# hooks) and OpenClaw (capture plugin with 7I context injection).
# This form is explicitly a last-resort for when someone has to feed
# in an external log and can't get the normal hooks to reach it.
feat: Phase 7I + UI refresh (capture form, memory/domain/activity pages, topnav) Closes three gaps the user surfaced: (1) OpenClaw agents run blind without AtoCore context, (2) mobile/desktop chats can't be captured at all, (3) wiki UI hadn't kept up with backend capabilities. Phase 7I — OpenClaw two-way bridge - Plugin now calls /context/build on before_agent_start and prepends the context pack to event.prompt, so whatever LLM runs underneath (sonnet, opus, codex, local model) answers grounded in AtoCore knowledge. Captured prompt stays the user's original text; fail-open with a 5s timeout. Config-gated via injectContext flag. - Plugin version 0.0.0 → 0.2.0; README rewritten. UI refresh - /wiki/capture — paste-to-ingest form for Claude Desktop / web / mobile / ChatGPT / other. Goes through normal /interactions pipeline with client="claude-desktop|claude-web|claude-mobile|chatgpt|other". Fixes the rotovap/mushroom-on-phone gap. - /wiki/memories/{id} (Phase 7E) — full memory detail: content, status, confidence, refs, valid_until, domain_tags (clickable to domain pages), project link, source chunk, graduated-to-entity link, full audit trail, related-by-tag neighbors. - /wiki/domains/{tag} (Phase 7F) — cross-project view: all active memories with the given tag grouped by project, sorted by count. Case-insensitive, whitespace-tolerant. Also surfaces graduated entities carrying the tag. - /wiki/activity — autonomous-activity timeline feed. Summary chips by action (created/promoted/merged/superseded/decayed/canonicalized) and by actor (auto-dedup-tier1, auto-dedup-tier2, confidence-decay, phase10-auto-promote, transient-to-durable, tag-canon, human-triage). Answers "what has the brain been doing while I was away?" - Home refresh: persistent topnav (Home · Activity · Capture · Triage · Dashboard), "What the brain is doing" snippet above project cards showing recent autonomous-actor counts, link to full activity. Tests: +10 (capture page, memory detail + 404, domain cross-project + empty + tag normalization, activity feed + groupings, home topnav, superseded-source detail after merge). 440 → 450. Known next: capture-browser extension for Claude.ai web (bigger project, deferred); voice/mobile relay (adjacent). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 10:14:15 -04:00
# ---------------------------------------------------------------------
def render_capture() -> str:
feat: Claude Code context injection (UserPromptSubmit hook) Closes the asymmetry the user surfaced: before this, Claude Code captured every turn (Stop hook) but retrieval only happened when Claude chose to call atocore_context (opt-in MCP tool). OpenClaw had both sides covered after 7I; Claude Code did not. Now symmetric. Every Claude Code prompt is auto-sent to /context/build and the returned pack is prepended via hookSpecificOutput.additionalContext — same as what OpenClaw's before_agent_start hook now does. - deploy/hooks/inject_context.py — UserPromptSubmit hook. Fail-open (always exit 0). Skips short/XML prompts. 5s timeout. Project inference mirrors capture_stop.py cwd→slug table. Kill switch: ATOCORE_CONTEXT_DISABLED=1. - ~/.claude/settings.json registered the hook (local config, not committed; copy-paste snippet in docs/capture-surfaces.md). - Removed /wiki/capture from topnav. Endpoint still exists but the page is now labeled "fallback only" with a warning banner. The sanctioned surfaces are Claude Code + OpenClaw; manual paste is explicitly not the design. - docs/capture-surfaces.md — scope statement: two surfaces, nothing else. Anthropic API polling explicitly prohibited. Tests: +8 for inject_context.py (exit 0 on all failure modes, kill switch, short prompt filter, XML filter, bad stdin, mock-server success shape, project inference from cwd). Updated 2 wiki tests for the topnav change. 450 → 459. Verified live with real AtoCore: injected 2979 chars of atocore project context on a cwd-matched prompt. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 12:01:41 -04:00
lines = ['<h1>📥 Manual capture (fallback only)</h1>']
feat: Phase 7I + UI refresh (capture form, memory/domain/activity pages, topnav) Closes three gaps the user surfaced: (1) OpenClaw agents run blind without AtoCore context, (2) mobile/desktop chats can't be captured at all, (3) wiki UI hadn't kept up with backend capabilities. Phase 7I — OpenClaw two-way bridge - Plugin now calls /context/build on before_agent_start and prepends the context pack to event.prompt, so whatever LLM runs underneath (sonnet, opus, codex, local model) answers grounded in AtoCore knowledge. Captured prompt stays the user's original text; fail-open with a 5s timeout. Config-gated via injectContext flag. - Plugin version 0.0.0 → 0.2.0; README rewritten. UI refresh - /wiki/capture — paste-to-ingest form for Claude Desktop / web / mobile / ChatGPT / other. Goes through normal /interactions pipeline with client="claude-desktop|claude-web|claude-mobile|chatgpt|other". Fixes the rotovap/mushroom-on-phone gap. - /wiki/memories/{id} (Phase 7E) — full memory detail: content, status, confidence, refs, valid_until, domain_tags (clickable to domain pages), project link, source chunk, graduated-to-entity link, full audit trail, related-by-tag neighbors. - /wiki/domains/{tag} (Phase 7F) — cross-project view: all active memories with the given tag grouped by project, sorted by count. Case-insensitive, whitespace-tolerant. Also surfaces graduated entities carrying the tag. - /wiki/activity — autonomous-activity timeline feed. Summary chips by action (created/promoted/merged/superseded/decayed/canonicalized) and by actor (auto-dedup-tier1, auto-dedup-tier2, confidence-decay, phase10-auto-promote, transient-to-durable, tag-canon, human-triage). Answers "what has the brain been doing while I was away?" - Home refresh: persistent topnav (Home · Activity · Capture · Triage · Dashboard), "What the brain is doing" snippet above project cards showing recent autonomous-actor counts, link to full activity. Tests: +10 (capture page, memory detail + 404, domain cross-project + empty + tag normalization, activity feed + groupings, home topnav, superseded-source detail after merge). 440 → 450. Known next: capture-browser extension for Claude.ai web (bigger project, deferred); voice/mobile relay (adjacent). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 10:14:15 -04:00
lines.append(
feat: Claude Code context injection (UserPromptSubmit hook) Closes the asymmetry the user surfaced: before this, Claude Code captured every turn (Stop hook) but retrieval only happened when Claude chose to call atocore_context (opt-in MCP tool). OpenClaw had both sides covered after 7I; Claude Code did not. Now symmetric. Every Claude Code prompt is auto-sent to /context/build and the returned pack is prepended via hookSpecificOutput.additionalContext — same as what OpenClaw's before_agent_start hook now does. - deploy/hooks/inject_context.py — UserPromptSubmit hook. Fail-open (always exit 0). Skips short/XML prompts. 5s timeout. Project inference mirrors capture_stop.py cwd→slug table. Kill switch: ATOCORE_CONTEXT_DISABLED=1. - ~/.claude/settings.json registered the hook (local config, not committed; copy-paste snippet in docs/capture-surfaces.md). - Removed /wiki/capture from topnav. Endpoint still exists but the page is now labeled "fallback only" with a warning banner. The sanctioned surfaces are Claude Code + OpenClaw; manual paste is explicitly not the design. - docs/capture-surfaces.md — scope statement: two surfaces, nothing else. Anthropic API polling explicitly prohibited. Tests: +8 for inject_context.py (exit 0 on all failure modes, kill switch, short prompt filter, XML filter, bad stdin, mock-server success shape, project inference from cwd). Updated 2 wiki tests for the topnav change. 450 → 459. Verified live with real AtoCore: injected 2979 chars of atocore project context on a cwd-matched prompt. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 12:01:41 -04:00
'<div class="triage-warning"><strong>This is not the capture path.</strong> '
'The sanctioned capture surfaces are Claude Code (Stop hook auto-captures every turn) '
'and OpenClaw (plugin auto-captures + injects AtoCore context on every agent turn). '
'This form exists only as a last resort for external logs you can\'t get into the normal pipeline.</div>'
)
lines.append(
'<p>If you\'re reaching for this page because you had a chat somewhere AtoCore didn\'t see, '
'fix the capture surface instead — don\'t paste. The deliberate scope is Claude Code + OpenClaw.</p>'
feat: Phase 7I + UI refresh (capture form, memory/domain/activity pages, topnav) Closes three gaps the user surfaced: (1) OpenClaw agents run blind without AtoCore context, (2) mobile/desktop chats can't be captured at all, (3) wiki UI hadn't kept up with backend capabilities. Phase 7I — OpenClaw two-way bridge - Plugin now calls /context/build on before_agent_start and prepends the context pack to event.prompt, so whatever LLM runs underneath (sonnet, opus, codex, local model) answers grounded in AtoCore knowledge. Captured prompt stays the user's original text; fail-open with a 5s timeout. Config-gated via injectContext flag. - Plugin version 0.0.0 → 0.2.0; README rewritten. UI refresh - /wiki/capture — paste-to-ingest form for Claude Desktop / web / mobile / ChatGPT / other. Goes through normal /interactions pipeline with client="claude-desktop|claude-web|claude-mobile|chatgpt|other". Fixes the rotovap/mushroom-on-phone gap. - /wiki/memories/{id} (Phase 7E) — full memory detail: content, status, confidence, refs, valid_until, domain_tags (clickable to domain pages), project link, source chunk, graduated-to-entity link, full audit trail, related-by-tag neighbors. - /wiki/domains/{tag} (Phase 7F) — cross-project view: all active memories with the given tag grouped by project, sorted by count. Case-insensitive, whitespace-tolerant. Also surfaces graduated entities carrying the tag. - /wiki/activity — autonomous-activity timeline feed. Summary chips by action (created/promoted/merged/superseded/decayed/canonicalized) and by actor (auto-dedup-tier1, auto-dedup-tier2, confidence-decay, phase10-auto-promote, transient-to-durable, tag-canon, human-triage). Answers "what has the brain been doing while I was away?" - Home refresh: persistent topnav (Home · Activity · Capture · Triage · Dashboard), "What the brain is doing" snippet above project cards showing recent autonomous-actor counts, link to full activity. Tests: +10 (capture page, memory detail + 404, domain cross-project + empty + tag normalization, activity feed + groupings, home topnav, superseded-source detail after merge). 440 → 450. Known next: capture-browser extension for Claude.ai web (bigger project, deferred); voice/mobile relay (adjacent). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 10:14:15 -04:00
)
lines.append('<p class="meta">Your prompt + the assistant\'s response. Project is optional — '
'the extractor infers it from content.</p>')
lines.append("""
<form id="capture-form" style="display:flex; flex-direction:column; gap:0.8rem; margin-top:1rem;">
<label><strong>Your prompt / question</strong>
<textarea id="cap-prompt" required rows="4"
style="width:100%; padding:0.6rem; background:var(--bg); color:var(--text); border:1px solid var(--border); border-radius:6px; font-family:inherit; font-size:0.95rem;"
placeholder="Paste what you asked…"></textarea>
</label>
<label><strong>Assistant response</strong>
<textarea id="cap-response" required rows="10"
style="width:100%; padding:0.6rem; background:var(--bg); color:var(--text); border:1px solid var(--border); border-radius:6px; font-family:inherit; font-size:0.95rem;"
placeholder="Paste the full assistant response…"></textarea>
</label>
<div style="display:flex; gap:0.5rem; align-items:center; flex-wrap:wrap;">
<label style="display:flex; gap:0.35rem; align-items:center;">Project (optional):
<input type="text" id="cap-project" placeholder="auto-detect"
style="padding:0.35rem 0.6rem; background:var(--bg); color:var(--text); border:1px solid var(--border); border-radius:4px; font-family:monospace; width:180px;">
</label>
<label style="display:flex; gap:0.35rem; align-items:center;">Source:
<select id="cap-source" style="padding:0.35rem; background:var(--bg); color:var(--text); border:1px solid var(--border); border-radius:4px;">
<option value="claude-desktop">Claude Desktop</option>
<option value="claude-web">Claude.ai web</option>
<option value="claude-mobile">Claude mobile</option>
<option value="chatgpt">ChatGPT</option>
<option value="other">Other</option>
</select>
</label>
</div>
<button type="submit"
style="padding:0.6rem 1.2rem; background:var(--accent); color:white; border:none; border-radius:6px; cursor:pointer; font-size:1rem; font-weight:600; align-self:flex-start;">
Save to AtoCore
</button>
</form>
<div id="cap-status" style="margin-top:1rem; font-size:0.9rem; min-height:1.5em;"></div>
<script>
document.getElementById('capture-form').addEventListener('submit', async (e) => {
e.preventDefault();
const prompt = document.getElementById('cap-prompt').value.trim();
const response = document.getElementById('cap-response').value.trim();
const project = document.getElementById('cap-project').value.trim();
const source = document.getElementById('cap-source').value;
const status = document.getElementById('cap-status');
if (!prompt || !response) { status.textContent = 'Need both prompt and response.'; return; }
status.textContent = 'Saving…';
try {
const r = await fetch('/interactions', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
prompt: prompt, response: response,
client: source, project: project, reinforce: true
})
});
if (r.ok) {
const data = await r.json();
status.innerHTML = '✅ Saved — interaction ' + (data.interaction_id || '?').slice(0,8) +
'. Runs through extraction + triage within the hour.<br>' +
'<a href="/interactions/' + (data.interaction_id || '') + '">view</a>';
document.getElementById('capture-form').reset();
} else {
status.textContent = '' + r.status + ': ' + (await r.text()).slice(0, 200);
}
} catch (err) { status.textContent = '' + err.message; }
});
</script>
""")
lines.append(
'<h2>How this works</h2>'
'<ul>'
'<li><strong>Claude Code</strong> → auto-captured via Stop hook</li>'
'<li><strong>OpenClaw</strong> → auto-captured + gets AtoCore context injected on prompt start (Phase 7I)</li>'
'<li><strong>Anything else</strong> (Claude Desktop, mobile, web, ChatGPT) → paste here</li>'
'</ul>'
'<p>The extractor is aggressive about capturing signal — don\'t hand-filter. '
'If the conversation had nothing durable, triage will auto-reject.</p>'
)
return render_html(
"Capture — AtoCore",
"\n".join(lines),
breadcrumbs=[("Wiki", "/wiki"), ("Capture", "")],
active_path="/wiki/capture",
)
# ---------------------------------------------------------------------
# Phase 7E — /wiki/memories/{id}: memory detail page
# ---------------------------------------------------------------------
def render_memory_detail(memory_id: str) -> str | None:
"""Full view of a single memory: content, audit trail, source refs,
neighbors, graduation status. Fills the drill-down gap the list
views can't."""
from atocore.memory.service import get_memory_audit
from atocore.models.database import get_connection
with get_connection() as conn:
row = conn.execute("SELECT * FROM memories WHERE id = ?", (memory_id,)).fetchone()
if row is None:
return None
import json as _json
mem = dict(row)
try:
tags = _json.loads(mem.get("domain_tags") or "[]") or []
except Exception:
tags = []
lines = [f'<h1>{mem["memory_type"]}: <span style="color:var(--text);">{mem["content"][:80]}</span></h1>']
if len(mem["content"]) > 80:
lines.append(f'<blockquote><p>{mem["content"]}</p></blockquote>')
# Metadata row
meta_items = [
f'<span class="tag">{mem["status"]}</span>',
f'<strong>{mem["memory_type"]}</strong>',
]
if mem.get("project"):
meta_items.append(f'<a href="/wiki/projects/{mem["project"]}">{mem["project"]}</a>')
meta_items.append(f'confidence: <strong>{float(mem.get("confidence") or 0):.2f}</strong>')
meta_items.append(f'refs: <strong>{int(mem.get("reference_count") or 0)}</strong>')
if mem.get("valid_until"):
meta_items.append(f'<span class="mem-expiry">valid until {str(mem["valid_until"])[:10]}</span>')
lines.append(f'<p>{" · ".join(meta_items)}</p>')
if tags:
tag_links = " ".join(f'<a href="/wiki/domains/{t}" class="tag-badge">{t}</a>' for t in tags)
lines.append(f'<p><span class="mem-tags">{tag_links}</span></p>')
lines.append(f'<p class="meta">id: <code>{mem["id"]}</code> · created: {mem["created_at"]}'
f' · updated: {mem.get("updated_at", "?")}'
+ (f' · last referenced: {mem["last_referenced_at"]}' if mem.get("last_referenced_at") else '')
+ '</p>')
# Graduation
if mem.get("graduated_to_entity_id"):
eid = mem["graduated_to_entity_id"]
lines.append(
f'<h2>🎓 Graduated</h2>'
f'<p>This memory was promoted to a typed entity: '
f'<a href="/wiki/entities/{eid}">{eid[:8]}</a></p>'
)
# Source chunk
if mem.get("source_chunk_id"):
lines.append(f'<h2>Source chunk</h2><p><code>{mem["source_chunk_id"]}</code></p>')
# Audit trail
audit = get_memory_audit(memory_id, limit=50)
if audit:
lines.append(f'<h2>Audit trail ({len(audit)} events)</h2><ul>')
for a in audit:
note = f'{a["note"]}' if a.get("note") else ""
lines.append(
f'<li><code>{a["timestamp"]}</code> '
f'<strong>{a["action"]}</strong> '
f'<em>{a["actor"]}</em>{note}</li>'
)
lines.append('</ul>')
# Neighbors by shared tag
if tags:
from atocore.memory.service import get_memories as _get_memories
neighbors = []
for t in tags[:3]:
for other in _get_memories(active_only=True, limit=30):
if other.id == memory_id:
continue
if any(ot == t for ot in (other.domain_tags or [])):
neighbors.append(other)
# Dedupe
seen = set()
uniq = []
for n in neighbors:
if n.id in seen:
continue
seen.add(n.id)
uniq.append(n)
if uniq:
lines.append(f'<h2>Related (by tag)</h2><ul>')
for n in uniq[:10]:
lines.append(
f'<li><a href="/wiki/memories/{n.id}">[{n.memory_type}] '
f'{n.content[:120]}</a>'
+ (f' <span class="tag">{n.project}</span>' if n.project else '')
+ '</li>'
)
lines.append('</ul>')
return render_html(
f"Memory {memory_id[:8]}",
"\n".join(lines),
breadcrumbs=[("Wiki", "/wiki"), ("Memory", "")],
)
# ---------------------------------------------------------------------
# Phase 7F — /wiki/domains/{tag}: cross-project domain view
# ---------------------------------------------------------------------
def render_domain(tag: str) -> str:
"""All memories + entities carrying a given domain_tag, grouped by project.
Answers 'what does the brain know about optics, across all projects?'"""
tag = (tag or "").strip().lower()
if not tag:
return render_html("Domain", "<p>No tag specified.</p>",
breadcrumbs=[("Wiki", "/wiki"), ("Domains", "")])
all_mems = get_memories(active_only=True, limit=500)
matching = [m for m in all_mems
if any((t or "").lower() == tag for t in (m.domain_tags or []))]
# Group by project
by_project: dict[str, list] = {}
for m in matching:
by_project.setdefault(m.project or "(global)", []).append(m)
lines = [f'<h1>Domain: <code>{tag}</code></h1>']
lines.append(f'<p class="meta">{len(matching)} active memories across {len(by_project)} projects</p>')
if not matching:
lines.append(
f'<p>No memories currently carry the tag <code>{tag}</code>.</p>'
'<p>Domain tags are assigned by the extractor when it identifies '
'the topical scope of a memory. They update over time.</p>'
)
return render_html(
f"Domain: {tag}",
"\n".join(lines),
breadcrumbs=[("Wiki", "/wiki"), ("Domains", ""), (tag, "")],
)
# Sort projects by count descending, (global) last
def sort_key(item: tuple[str, list]) -> tuple[int, int]:
proj, mems = item
return (1 if proj == "(global)" else 0, -len(mems))
for proj, mems in sorted(by_project.items(), key=sort_key):
proj_link = proj if proj == "(global)" else f'<a href="/wiki/projects/{proj}">{proj}</a>'
lines.append(f'<h2>{proj_link} ({len(mems)})</h2><ul>')
for m in mems:
other_tags = [t for t in (m.domain_tags or []) if t != tag][:3]
other_tags_html = ""
if other_tags:
other_tags_html = ' <span class="mem-tags">' + " ".join(
f'<a href="/wiki/domains/{t}" class="tag-badge">{t}</a>' for t in other_tags
) + '</span>'
lines.append(
f'<li><a href="/wiki/memories/{m.id}">[{m.memory_type}] '
f'{m.content[:200]}</a>'
f' <span class="meta">conf {m.confidence:.2f} · refs {m.reference_count}</span>'
f'{other_tags_html}</li>'
)
lines.append('</ul>')
# Entities with this tag (if any have tags — currently they might not)
try:
all_entities = get_entities(limit=500)
ent_matching = []
for e in all_entities:
tags = e.properties.get("domain_tags") if e.properties else []
if isinstance(tags, list) and tag in [str(t).lower() for t in tags]:
ent_matching.append(e)
if ent_matching:
lines.append(f'<h2>🔧 Entities ({len(ent_matching)})</h2><ul>')
for e in ent_matching:
lines.append(
f'<li><a href="/wiki/entities/{e.id}">[{e.entity_type}] {e.name}</a>'
+ (f' <span class="tag">{e.project}</span>' if e.project else '')
+ '</li>'
)
lines.append('</ul>')
except Exception:
pass
return render_html(
f"Domain: {tag}",
"\n".join(lines),
breadcrumbs=[("Wiki", "/wiki"), ("Domains", ""), (tag, "")],
)
# ---------------------------------------------------------------------
# /wiki/activity — autonomous-activity feed
# ---------------------------------------------------------------------
def render_activity(hours: int = 48, limit: int = 100) -> str:
"""Timeline of what the autonomous pipeline did recently. Answers
'what has the brain been doing while I was away?'"""
from atocore.memory.service import get_recent_audit
audit = get_recent_audit(limit=limit)
# Group events by category for summary
by_action: dict[str, int] = {}
by_actor: dict[str, int] = {}
for a in audit:
by_action[a["action"]] = by_action.get(a["action"], 0) + 1
by_actor[a["actor"]] = by_actor.get(a["actor"], 0) + 1
lines = [f'<h1>📡 Activity Feed</h1>']
lines.append(f'<p class="meta">Last {len(audit)} events in the memory audit log</p>')
# Summary chips
if by_action or by_actor:
lines.append('<h2>Summary</h2>')
lines.append('<p><strong>By action:</strong> ' +
" · ".join(f'{k}: {v}' for k, v in sorted(by_action.items(), key=lambda x: -x[1])) +
'</p>')
lines.append('<p><strong>By actor:</strong> ' +
" · ".join(f'<code>{k}</code>: {v}' for k, v in sorted(by_actor.items(), key=lambda x: -x[1])) +
'</p>')
# Action-type color/emoji
action_emoji = {
"created": "", "promoted": "", "rejected": "", "invalidated": "🚫",
"superseded": "🔀", "reinforced": "🔁", "updated": "✏️",
"auto_promoted": "", "created_via_merge": "🔗",
"valid_until_extended": "", "tag_canonicalized": "🏷️",
}
lines.append('<h2>Timeline</h2><ul>')
for a in audit:
emoji = action_emoji.get(a["action"], "")
preview = a.get("content_preview") or ""
ts_short = a["timestamp"][:16] if a.get("timestamp") else "?"
mid_short = (a.get("memory_id") or "")[:8]
note = f' — <em>{a["note"]}</em>' if a.get("note") else ""
lines.append(
f'<li>{emoji} <code>{ts_short}</code> '
f'<strong>{a["action"]}</strong> '
f'<em>{a["actor"]}</em> '
f'<a href="/wiki/memories/{a["memory_id"]}">{mid_short}</a>'
f'{note}'
+ (f'<br><span style="opacity:0.6; font-size:0.85rem; margin-left:1.5rem;">{preview[:140]}</span>' if preview else '')
+ '</li>'
)
lines.append('</ul>')
return render_html(
"Activity — AtoCore",
"\n".join(lines),
breadcrumbs=[("Wiki", "/wiki"), ("Activity", "")],
active_path="/wiki/activity",
)
_TEMPLATE = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{title}} AtoCore</title>
<style>
:root { --bg: #fafafa; --text: #1a1a2e; --accent: #2563eb; --border: #e2e8f0; --card: #fff; --hover: #f1f5f9; }
@media (prefers-color-scheme: dark) {
:root { --bg: #0f172a; --text: #e2e8f0; --accent: #60a5fa; --border: #334155; --card: #1e293b; --hover: #334155; }
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.7; color: var(--text); background: var(--bg);
max-width: 800px; margin: 0 auto; padding: 1.5rem;
}
h1 { font-size: 1.8rem; margin-bottom: 0.5rem; color: var(--accent); }
h2 { font-size: 1.3rem; margin-top: 2rem; margin-bottom: 0.6rem; padding-bottom: 0.2rem; border-bottom: 2px solid var(--border); }
h3 { font-size: 1.1rem; margin-top: 1.2rem; margin-bottom: 0.4rem; }
p { margin-bottom: 0.8rem; }
ul { margin-left: 1.5rem; margin-bottom: 1rem; }
li { margin-bottom: 0.3rem; }
li ul { margin-top: 0.2rem; }
strong { color: var(--accent); font-weight: 600; }
em { opacity: 0.7; font-size: 0.9em; }
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
blockquote {
background: var(--card); border-left: 4px solid var(--accent);
padding: 0.6rem 1rem; margin: 1rem 0; border-radius: 0 6px 6px 0;
font-size: 0.9em;
}
hr { border: none; border-top: 1px solid var(--border); margin: 2rem 0; }
.breadcrumbs { margin-bottom: 1.5rem; font-size: 0.85em; opacity: 0.7; }
.breadcrumbs a { opacity: 0.8; }
feat: Phase 7I + UI refresh (capture form, memory/domain/activity pages, topnav) Closes three gaps the user surfaced: (1) OpenClaw agents run blind without AtoCore context, (2) mobile/desktop chats can't be captured at all, (3) wiki UI hadn't kept up with backend capabilities. Phase 7I — OpenClaw two-way bridge - Plugin now calls /context/build on before_agent_start and prepends the context pack to event.prompt, so whatever LLM runs underneath (sonnet, opus, codex, local model) answers grounded in AtoCore knowledge. Captured prompt stays the user's original text; fail-open with a 5s timeout. Config-gated via injectContext flag. - Plugin version 0.0.0 → 0.2.0; README rewritten. UI refresh - /wiki/capture — paste-to-ingest form for Claude Desktop / web / mobile / ChatGPT / other. Goes through normal /interactions pipeline with client="claude-desktop|claude-web|claude-mobile|chatgpt|other". Fixes the rotovap/mushroom-on-phone gap. - /wiki/memories/{id} (Phase 7E) — full memory detail: content, status, confidence, refs, valid_until, domain_tags (clickable to domain pages), project link, source chunk, graduated-to-entity link, full audit trail, related-by-tag neighbors. - /wiki/domains/{tag} (Phase 7F) — cross-project view: all active memories with the given tag grouped by project, sorted by count. Case-insensitive, whitespace-tolerant. Also surfaces graduated entities carrying the tag. - /wiki/activity — autonomous-activity timeline feed. Summary chips by action (created/promoted/merged/superseded/decayed/canonicalized) and by actor (auto-dedup-tier1, auto-dedup-tier2, confidence-decay, phase10-auto-promote, transient-to-durable, tag-canon, human-triage). Answers "what has the brain been doing while I was away?" - Home refresh: persistent topnav (Home · Activity · Capture · Triage · Dashboard), "What the brain is doing" snippet above project cards showing recent autonomous-actor counts, link to full activity. Tests: +10 (capture page, memory detail + 404, domain cross-project + empty + tag normalization, activity feed + groupings, home topnav, superseded-source detail after merge). 440 → 450. Known next: capture-browser extension for Claude.ai web (bigger project, deferred); voice/mobile relay (adjacent). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 10:14:15 -04:00
.topnav { display: flex; gap: 0.25rem; flex-wrap: wrap; margin-bottom: 1rem; padding-bottom: 0.8rem; border-bottom: 1px solid var(--border); }
.topnav-item { padding: 0.35rem 0.8rem; background: var(--card); border: 1px solid var(--border); border-radius: 6px; font-size: 0.88rem; color: var(--text); opacity: 0.75; text-decoration: none; }
.topnav-item:hover { opacity: 1; background: var(--hover); text-decoration: none; }
.topnav-item.active { background: var(--accent); color: white; border-color: var(--accent); opacity: 1; }
.topnav-item.active:hover { background: var(--accent); }
.activity-snippet { background: var(--card); border: 1px solid var(--border); border-radius: 8px; padding: 1rem; margin: 1rem 0; }
.activity-snippet h3 { color: var(--accent); margin-bottom: 0.4rem; }
.activity-snippet ul { margin: 0.3rem 0 0 1.2rem; font-size: 0.9rem; }
.activity-snippet li { margin-bottom: 0.2rem; }
.stat-row { display: flex; gap: 1rem; flex-wrap: wrap; font-size: 0.9rem; margin: 0.4rem 0; }
.stat-row span { padding: 0.1rem 0.4rem; background: var(--hover); border-radius: 4px; }
.meta { font-size: 0.8em; opacity: 0.5; margin-top: 0.5rem; }
.wikilink { color: var(--accent); text-decoration: none; border-bottom: 1px dashed transparent; }
.wikilink:hover { border-bottom-color: var(--accent); }
.wikilink-cross { border-bottom-style: dotted; }
.wikilink-scope { font-size: 0.75em; opacity: 0.6; font-style: italic; }
.redlink { color: #d0473d; text-decoration: none; font-style: italic; border-bottom: 1px dashed #d0473d; }
.redlink:hover { background: rgba(208, 71, 61, 0.08); }
.new-entity-form { display: flex; flex-direction: column; gap: 0.9rem; max-width: 520px; background: var(--card); border: 1px solid var(--border); border-radius: 8px; padding: 1rem 1.2rem; }
.new-entity-form label { display: flex; flex-direction: column; font-size: 0.88rem; opacity: 0.8; }
.new-entity-form input, .new-entity-form select, .new-entity-form textarea { margin-top: 0.3rem; padding: 0.45rem 0.6rem; background: var(--bg); border: 1px solid var(--border); border-radius: 4px; color: var(--text); font-size: 0.95rem; }
.new-entity-form button { align-self: flex-start; padding: 0.5rem 1.1rem; background: var(--accent); color: white; border: none; border-radius: 6px; cursor: pointer; }
.new-entity-result { font-size: 0.9rem; opacity: 0.85; min-height: 1em; }
feat(assets): binary asset store + artifact entity + wiki evidence (Issue F) Wires visual evidence into the knowledge graph. Images, PDFs, and CAD exports can now be uploaded, deduped by SHA-256, thumbnailed, linked to entities via EVIDENCED_BY, and rendered inline on wiki pages. Unblocks AKC uploading voice-session screenshots alongside extracted entities. - assets/ module: store_asset (hash dedup + MIME allowlist + 20 MB cap), get_asset_binary, get_thumbnail (Pillow, on-disk cache under .thumbnails/<size>/), list_orphan_assets, invalidate_asset - models/database.py: new `assets` table + indexes - engineering/service.py: `artifact` added to ENTITY_TYPES - api/routes.py: POST /assets (multipart), GET /assets/{id}, /assets/{id}/thumbnail, /assets/{id}/meta, /admin/assets/orphans, DELETE /assets/{id} (409 if still referenced), GET /entities/{id}/evidence (EVIDENCED_BY artifacts with asset meta) - main.py: all new paths aliased under /v1 - engineering/wiki.py: entity pages render EVIDENCED_BY → artifact as a "Visual evidence" thumbnail strip; artifact pages render the full image + caption + capture_context - deploy/dalidou/docker-compose.yml: bind-mount ${ATOCORE_ASSETS_DIR} - config.py: assets_dir + assets_max_upload_bytes settings - requirements.txt + pyproject.toml: python-multipart, Pillow>=10.0.0 - tests/test_assets.py: 16 tests (dedup, cap, thumbnail cache, orphan detection, invalidate gating, API upload/fetch, evidence, v1 aliases, wiki rendering) - DEV-LEDGER.md: session log + cleanup note + test_count 478 -> 494 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 21:46:52 -04:00
.evidence-strip { display: flex; flex-wrap: wrap; gap: 0.75rem; margin: 0.75rem 0 1.25rem; }
.evidence-tile { margin: 0; background: var(--card); border: 1px solid var(--border); border-radius: 6px; padding: 0.4rem; max-width: 270px; }
.evidence-tile img { display: block; max-width: 100%; height: auto; border-radius: 3px; }
.evidence-tile figcaption { font-size: 0.8rem; margin-top: 0.35rem; opacity: 0.85; }
.evidence-pdf, .evidence-other { padding: 0.6rem 0.8rem; font-size: 0.9rem; }
.artifact-full figure, .artifact-full { margin: 0 0 1rem; }
.artifact-full img { display: block; max-width: 100%; height: auto; border: 1px solid var(--border); border-radius: 4px; }
.artifact-full figcaption { font-size: 0.9rem; margin-top: 0.5rem; opacity: 0.85; }
.tag { background: var(--accent); color: var(--bg); padding: 0.1rem 0.4rem; border-radius: 3px; font-size: 0.75em; margin-left: 0.3rem; }
.search-box { display: flex; gap: 0.5rem; margin: 1.5rem 0; }
.search-box input {
flex: 1; padding: 0.6rem 1rem; border: 2px solid var(--border);
border-radius: 8px; background: var(--card); color: var(--text);
font-size: 1rem;
}
.search-box input:focus { border-color: var(--accent); outline: none; }
.search-box button {
padding: 0.6rem 1.2rem; background: var(--accent); color: var(--bg);
border: none; border-radius: 8px; cursor: pointer; font-size: 1rem;
}
.card-grid { display: grid; grid-template-columns: 1fr; gap: 1rem; margin: 1rem 0; }
@media (min-width: 600px) { .card-grid { grid-template-columns: 1fr 1fr; } }
.card {
display: block; background: var(--card); border: 1px solid var(--border);
border-radius: 10px; padding: 1.2rem; text-decoration: none;
color: var(--text); transition: border-color 0.2s;
}
.card:hover { border-color: var(--accent); background: var(--hover); text-decoration: none; }
.card h3 { color: var(--accent); margin: 0 0 0.3rem 0; }
.card p { font-size: 0.9em; margin: 0; opacity: 0.8; }
.card .stats { font-size: 0.8em; margin-top: 0.5rem; opacity: 0.5; }
.card .client { font-size: 0.85em; opacity: 0.65; margin-bottom: 0.3rem; font-style: italic; }
.card h3 .tag { font-size: 0.65em; vertical-align: middle; margin-left: 0.4rem; }
.triage-notice { background: var(--card); border-left: 4px solid var(--accent); padding: 0.6rem 1rem; border-radius: 4px; margin: 0.8rem 0; }
.triage-warning { background: #fef3c7; color: #78350f; border-left: 4px solid #d97706; padding: 0.6rem 1rem; border-radius: 4px; margin: 0.8rem 0; }
@media (prefers-color-scheme: dark) { .triage-warning { background: #451a03; color: #fde68a; } }
feat: Phase 3 V1 — Auto-Organization (domain_tags + valid_until) Adds structural metadata that the LLM triage was already implicitly reasoning about ("stale snapshot" → reject). Phase 3 captures that reasoning as fields so it can DRIVE retrieval, not just rejection. Schema (src/atocore/models/database.py): - domain_tags TEXT DEFAULT '[]' JSON array of lowercase topic keywords - valid_until DATETIME ISO date; null = permanent - idx_memories_valid_until index for efficient expiry queries Memory service (src/atocore/memory/service.py): - Memory dataclass gains domain_tags + valid_until - create_memory, update_memory accept/persist both - _row_to_memory safely reads both (JSON-decode + null handling) - _normalize_tags helper: lowercase, dedup, strip, cap at 10 - get_memories_for_context filters expired (valid_until < today UTC) - _rank_memories_for_query adds tag-boost: memories whose domain_tags appear as substrings in query text rank higher (tertiary key after content-overlap density + absolute overlap, before confidence) LLM extractor (_llm_prompt.py → llm-0.5.0): - SYSTEM_PROMPT documents domain_tags (2-5 keywords) + valid_until (time-bounded facts get expiry dates; durable facts stay null) - normalize_candidate_item parses both fields from model output with graceful fallback for string/null/missing LLM triage (scripts/auto_triage.py): - TRIAGE_SYSTEM_PROMPT documents same two fields - parse_verdict extracts them from verdict JSON - On promote: PUT /memory/{id} with tags + valid_until BEFORE POST /memory/{id}/promote, so active memories carry them API (src/atocore/api/routes.py): - MemoryCreateRequest: adds domain_tags, valid_until - MemoryUpdateRequest: adds domain_tags, valid_until, memory_type - GET /memory response exposes domain_tags + valid_until + created_at Triage UI (src/atocore/engineering/triage_ui.py): - Renders existing tags as colored badges - Adds inline text field for tags (comma-separated) + date picker for valid_until on every candidate card - Save&Promote button persists edits via PUT then promotes - Plain Promote (and Y shortcut) also saves tags/expiry if edited Wiki (src/atocore/engineering/wiki.py): - Search now matches memory content OR domain_tags - Search results render tags as clickable badges linking to /wiki/search?q=<tag> for cross-project navigation - valid_until shown as amber "valid until YYYY-MM-DD" hint Tests: 303 → 308 (5 new for Phase 3 behavior): - test_create_memory_with_tags_and_valid_until - test_create_memory_normalizes_tags - test_update_memory_sets_tags_and_valid_until - test_get_memories_for_context_excludes_expired - test_context_builder_tag_boost_orders_results Deferred (explicitly): temporal_scope enum, source_refs memory graph, HDBSCAN clustering, memory detail wiki page, backfill of existing actives. See docs/MASTER-BRAIN-PLAN.md. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 21:37:01 -04:00
.mem-tags { display: inline-flex; gap: 0.25rem; flex-wrap: wrap; vertical-align: middle; }
.tag-badge { background: var(--accent); color: white; padding: 0.1rem 0.5rem; border-radius: 10px; font-size: 0.7rem; font-family: monospace; text-decoration: none; font-weight: 500; }
.tag-badge:hover { opacity: 0.85; text-decoration: none; }
.mem-expiry { font-size: 0.75rem; color: #d97706; font-style: italic; margin-left: 0.4rem; }
@media (prefers-color-scheme: dark) { .mem-expiry { color: #fbbf24; } }
feat: Phase 6 — Living Taxonomy + Universal Capture Closes two real-use gaps: 1. "APM tool" gap: work done outside Claude Code (desktop, web, phone, other machine) was invisible to AtoCore. 2. Project discovery gap: manual JSON-file edits required to promote an emerging theme to a first-class project. B — atocore_remember MCP tool (scripts/atocore_mcp.py): - New MCP tool for universal capture from any MCP-aware client (Claude Desktop, Code, Cursor, Zed, Windsurf, etc.) - Accepts content (required) + memory_type/project/confidence/ valid_until/domain_tags (all optional with sensible defaults) - Creates a candidate memory, goes through the existing 3-tier triage (no bypass — the quality gate catches noise) - Detailed tool description guides Claude on when to invoke: "remember this", "save that for later", "don't lose this fact" - Total tools exposed by MCP server: 14 → 15 C.1 Emerging-concepts detector (scripts/detect_emerging.py): - Nightly scan of active + candidate memories for: * Unregistered project names with ≥3 memory occurrences * Top 20 domain_tags by frequency (emerging categories) * Active memories with reference_count ≥ 5 + valid_until set (reinforced transients — candidates for extension) - Writes findings to atocore/proposals/* project state entries - Emits "warning" alert via Phase 4 framework the FIRST time a new project crosses the 5-memory alert threshold (avoids spam) - Configurable via env vars: ATOCORE_EMERGING_PROJECT_MIN (default 3), ATOCORE_EMERGING_ALERT_THRESHOLD (default 5), TOP_TAGS_LIMIT (20) C.2 Registration surface (src/atocore/api/routes.py + wiki.py): - POST /admin/projects/register-emerging — one-click register with sensible defaults (ingest_roots auto-filled with vault:incoming/projects/<id>/ convention). Clears the proposal from the dashboard list on success. - Dashboard /admin/dashboard: new "proposals" section with unregistered_projects + emerging_categories + reinforced_transients. - Wiki homepage: "📋 Emerging" section rendering each unregistered project as a card with count + 2 sample memory previews + inline "📌 Register as project" button that calls the endpoint via fetch, reloads the page on success. C.3 Transient-to-durable extension (src/atocore/memory/service.py + API + cron): - New extend_reinforced_valid_until() function — scans active memories with valid_until in the next 30 days and reference_count ≥ 5. Extends expiry by 90 days. If reference_count ≥ 10, clears expiry entirely (makes permanent). Writes audit rows via the Phase 4 memory_audit framework with actor="transient-to-durable". - POST /admin/memory/extend-reinforced — API wrapper for cron. - Matches the user's intuition: "something transient becomes important if you keep coming back to it". Nightly cron (deploy/dalidou/batch-extract.sh): - Step F2: detect_emerging.py (after F pipeline summary) - Step F3: /admin/memory/extend-reinforced (before integrity check) - Both fail-open; errors don't break the pipeline. Tests: 366 → 374 (+8 for Phase 6): - 6 tests for extend_reinforced_valid_until covering: extension path, permanent path, skip far-future, skip low-refs, skip permanent memories, audit row write - 2 smoke tests for the detector (imports cleanly, handles empty DB) - MCP tool changes don't need new tests — the wrapper is pure passthrough Design decisions documented in plan file: - atocore_remember deliberately doesn't bypass triage (quality gate) - Detector is passive (surfaces proposals) not active (auto-registers) - Sensible ingest-root defaults ("vault:incoming/projects/<id>/") so registration is one-click with no file-path thinking - Extension adds 90 days rather than clearing expiry (gradual permanence earned through sustained reinforcement) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 08:08:55 -04:00
/* Phase 6 C.2 Emerging projects section */
.emerging-intro { font-size: 0.9rem; opacity: 0.75; margin-bottom: 0.8rem; }
.emerging-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 1rem; margin-bottom: 1rem; }
.emerging-card { background: var(--card); border: 1px dashed var(--accent); border-radius: 8px; padding: 1rem; }
.emerging-card h3 { margin: 0 0 0.3rem 0; color: var(--accent); font-family: monospace; font-size: 1rem; }
.emerging-count { font-size: 0.8rem; opacity: 0.6; margin-bottom: 0.5rem; }
.emerging-samples { font-size: 0.85rem; margin: 0.5rem 0; padding-left: 1.2rem; opacity: 0.8; }
.emerging-samples li { margin-bottom: 0.25rem; }
.btn-register-emerging { width: 100%; padding: 0.45rem 0.9rem; background: var(--accent); color: white; border: 1px solid var(--accent); border-radius: 4px; cursor: pointer; font-size: 0.88rem; font-weight: 500; margin-top: 0.5rem; }
.btn-register-emerging:hover { opacity: 0.9; }
</style>
feat: Phase 6 — Living Taxonomy + Universal Capture Closes two real-use gaps: 1. "APM tool" gap: work done outside Claude Code (desktop, web, phone, other machine) was invisible to AtoCore. 2. Project discovery gap: manual JSON-file edits required to promote an emerging theme to a first-class project. B — atocore_remember MCP tool (scripts/atocore_mcp.py): - New MCP tool for universal capture from any MCP-aware client (Claude Desktop, Code, Cursor, Zed, Windsurf, etc.) - Accepts content (required) + memory_type/project/confidence/ valid_until/domain_tags (all optional with sensible defaults) - Creates a candidate memory, goes through the existing 3-tier triage (no bypass — the quality gate catches noise) - Detailed tool description guides Claude on when to invoke: "remember this", "save that for later", "don't lose this fact" - Total tools exposed by MCP server: 14 → 15 C.1 Emerging-concepts detector (scripts/detect_emerging.py): - Nightly scan of active + candidate memories for: * Unregistered project names with ≥3 memory occurrences * Top 20 domain_tags by frequency (emerging categories) * Active memories with reference_count ≥ 5 + valid_until set (reinforced transients — candidates for extension) - Writes findings to atocore/proposals/* project state entries - Emits "warning" alert via Phase 4 framework the FIRST time a new project crosses the 5-memory alert threshold (avoids spam) - Configurable via env vars: ATOCORE_EMERGING_PROJECT_MIN (default 3), ATOCORE_EMERGING_ALERT_THRESHOLD (default 5), TOP_TAGS_LIMIT (20) C.2 Registration surface (src/atocore/api/routes.py + wiki.py): - POST /admin/projects/register-emerging — one-click register with sensible defaults (ingest_roots auto-filled with vault:incoming/projects/<id>/ convention). Clears the proposal from the dashboard list on success. - Dashboard /admin/dashboard: new "proposals" section with unregistered_projects + emerging_categories + reinforced_transients. - Wiki homepage: "📋 Emerging" section rendering each unregistered project as a card with count + 2 sample memory previews + inline "📌 Register as project" button that calls the endpoint via fetch, reloads the page on success. C.3 Transient-to-durable extension (src/atocore/memory/service.py + API + cron): - New extend_reinforced_valid_until() function — scans active memories with valid_until in the next 30 days and reference_count ≥ 5. Extends expiry by 90 days. If reference_count ≥ 10, clears expiry entirely (makes permanent). Writes audit rows via the Phase 4 memory_audit framework with actor="transient-to-durable". - POST /admin/memory/extend-reinforced — API wrapper for cron. - Matches the user's intuition: "something transient becomes important if you keep coming back to it". Nightly cron (deploy/dalidou/batch-extract.sh): - Step F2: detect_emerging.py (after F pipeline summary) - Step F3: /admin/memory/extend-reinforced (before integrity check) - Both fail-open; errors don't break the pipeline. Tests: 366 → 374 (+8 for Phase 6): - 6 tests for extend_reinforced_valid_until covering: extension path, permanent path, skip far-future, skip low-refs, skip permanent memories, audit row write - 2 smoke tests for the detector (imports cleanly, handles empty DB) - MCP tool changes don't need new tests — the wrapper is pure passthrough Design decisions documented in plan file: - atocore_remember deliberately doesn't bypass triage (quality gate) - Detector is passive (surfaces proposals) not active (auto-registers) - Sensible ingest-root defaults ("vault:incoming/projects/<id>/") so registration is one-click with no file-path thinking - Extension adds 90 days rather than clearing expiry (gradual permanence earned through sustained reinforcement) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-18 08:08:55 -04:00
<script>
async function registerEmerging(projectId) {
if (!confirm(`Register "${projectId}" as a first-class project?\n\nThis creates:\n /wiki/projects/${projectId} page\n System map + gaps + killer queries\n Triage + graduation support\n\nIngest root defaults to vault:incoming/projects/${projectId}/`)) {
return;
}
try {
const r = await fetch('/admin/projects/register-emerging', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({project_id: projectId}),
});
if (r.ok) {
const data = await r.json();
alert(data.message || `Registered ${projectId}`);
window.location.reload();
} else {
const err = await r.text();
alert(`Registration failed: ${r.status}\n${err.substring(0, 300)}`);
}
} catch (e) {
alert(`Network error: ${e.message}`);
}
}
</script>
</head>
<body>
{{nav}}
{{body}}
</body>
</html>"""