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>
This commit is contained in:
2026-04-18 08:08:55 -04:00
parent cc68839306
commit 02055e8db3
7 changed files with 736 additions and 0 deletions

View File

@@ -243,6 +243,72 @@ def _tool_projects(args: dict) -> str:
return "\n".join(lines)
def _tool_remember(args: dict) -> str:
"""Phase 6 Part B — universal capture from any Claude session.
Wraps POST /memory to create a candidate memory tagged with
source='mcp-remember'. The existing 3-tier triage is the quality
gate: nothing becomes active until sonnet (+ opus if borderline)
approves it. Returns the memory id so the caller can reference it
in the same session.
"""
content = (args.get("content") or "").strip()
if not content:
return "Error: 'content' is required."
memory_type = (args.get("memory_type") or "knowledge").strip()
valid_types = ["identity", "preference", "project", "episodic", "knowledge", "adaptation"]
if memory_type not in valid_types:
return f"Error: memory_type must be one of {valid_types}."
project = (args.get("project") or "").strip()
try:
confidence = float(args.get("confidence") or 0.6)
except (TypeError, ValueError):
confidence = 0.6
confidence = max(0.0, min(1.0, confidence))
valid_until = (args.get("valid_until") or "").strip()
tags = args.get("domain_tags") or []
if not isinstance(tags, list):
tags = []
# Normalize tags: lowercase, dedupe, cap at 10
clean_tags: list[str] = []
for t in tags[:10]:
if not isinstance(t, str):
continue
t = t.strip().lower()
if t and t not in clean_tags:
clean_tags.append(t)
payload = {
"memory_type": memory_type,
"content": content,
"project": project,
"confidence": confidence,
"status": "candidate",
}
if valid_until:
payload["valid_until"] = valid_until
if clean_tags:
payload["domain_tags"] = clean_tags
result, err = safe_call(http_post, "/memory", payload)
if err:
return f"AtoCore remember failed: {err}"
mid = result.get("id", "?")
scope = project if project else "(global)"
tag_str = f" tags=[{', '.join(clean_tags)}]" if clean_tags else ""
expires = f" valid_until={valid_until}" if valid_until else ""
return (
f"Remembered as candidate: id={mid}\n"
f" type={memory_type} project={scope} confidence={confidence:.2f}{tag_str}{expires}\n"
f"Will flow through the standard triage pipeline within 24h "
f"(or on next auto-process button click at /admin/triage)."
)
def _tool_health(args: dict) -> str:
"""Check AtoCore service health."""
result, err = safe_call(http_get, "/health")
@@ -527,6 +593,58 @@ TOOLS = [
},
"handler": _tool_memory_create,
},
{
"name": "atocore_remember",
"description": (
"Save a durable fact to AtoCore's memory layer from any conversation. "
"Use when the user says 'remember this', 'save that for later', "
"'don't lose this fact', or when you identify a decision/insight/"
"preference worth persisting across future sessions. The fact "
"goes through quality review before being consulted in future "
"context packs (so durable facts get kept, noise gets rejected). "
"Call multiple times if one conversation has multiple distinct "
"facts worth remembering — one tool call per atomic fact. "
"Prefer 'knowledge' type for cross-project engineering insights, "
"'project' for facts specific to one project, 'preference' for "
"user work-style notes, 'adaptation' for standing behavioral rules."
),
"inputSchema": {
"type": "object",
"properties": {
"content": {
"type": "string",
"description": "The atomic fact to remember. Under 250 chars. Should stand alone without session context.",
},
"memory_type": {
"type": "string",
"enum": ["identity", "preference", "project", "episodic", "knowledge", "adaptation"],
"default": "knowledge",
},
"project": {
"type": "string",
"description": "Project id if scoped. Empty for cross-project. Unregistered names flagged by triage as 'emerging project' proposals.",
},
"confidence": {
"type": "number",
"minimum": 0,
"maximum": 1,
"default": 0.6,
"description": "0.5-0.7 typical. 0.8+ only for ratified/committed claims.",
},
"valid_until": {
"type": "string",
"description": "ISO date YYYY-MM-DD if time-bounded (e.g. current state, scheduled event, quote expiry). Empty for permanent facts.",
},
"domain_tags": {
"type": "array",
"items": {"type": "string"},
"description": "Lowercase topical tags (optics, thermal, firmware, procurement, etc.) for cross-project retrieval. 2-5 tags typical.",
},
},
"required": ["content"],
},
"handler": _tool_remember,
},
{
"name": "atocore_project_state",
"description": (