diff --git a/packages/openclaw-plugin/src/hooks/recall.ts b/packages/openclaw-plugin/src/hooks/recall.ts index 148977a..44046c1 100644 --- a/packages/openclaw-plugin/src/hooks/recall.ts +++ b/packages/openclaw-plugin/src/hooks/recall.ts @@ -374,10 +374,11 @@ export function rerankByTypeWeight( const type = r.type || "memory"; const weight = weights[type] ?? 1.0; const recency = calcRecencyBoost(r.created, recencyBoost); - // Manual entries (no auto-capture tag) get a boost over auto-captured ones. + // Human-created entries (webui/cli tags) get a boost over auto-captured and agent entries. // This ensures intentionally stored knowledge ranks higher than conversation noise. - const isAutoCapture = r.tags?.includes("auto-capture") ?? false; - const sourceBoost = isAutoCapture ? 1.0 : manualEntryBoost; + const tags = r.tags ?? []; + const isHumanCreated = tags.includes("webui") || tags.includes("cli"); + const sourceBoost = isHumanCreated ? manualEntryBoost : 1.0; return { id: r.id, body: r.content || r.body || "", diff --git a/palaia/cli.py b/palaia/cli.py index 2703ec5..9f72518 100644 --- a/palaia/cli.py +++ b/palaia/cli.py @@ -212,14 +212,17 @@ def cmd_write(args): root = get_root() agent = _resolve_agent(args) instance = _resolve_instance_for_write(args) - tags = args.tags.split(",") if args.tags else None + tags = args.tags.split(",") if args.tags else [] + # Tag entries created via CLI so recall can distinguish source + if "cli" not in tags: + tags.append("cli") result = write_entry( root, body=args.text, scope=args.scope, agent=agent, - tags=tags, + tags=tags or None, title=args.title, project=getattr(args, "project", None), entry_type=getattr(args, "type", None), diff --git a/palaia/web/routes/entries.py b/palaia/web/routes/entries.py index e32ebdc..fcee801 100644 --- a/palaia/web/routes/entries.py +++ b/palaia/web/routes/entries.py @@ -1,9 +1,10 @@ """Entry CRUD routes. -v2.6 semantics: +v2.6+ semantics: - Tasks are post-its: setting status=done/wontfix on a task deletes it. -- Auto-capture entries carry the 'auto-capture' tag; manual entries do not. - Manual entries rank 30% higher in recall (visible via is_manual flag). +- Source tags: 'webui' (created in browser), 'cli' (palaia add/write), + 'auto-capture' (passive capture). No source tag = agent (MCP/tool). + Human-created entries (webui, cli) rank 30% higher in recall. """ from __future__ import annotations @@ -57,10 +58,21 @@ def _validate_enum(value: str | None, valid: set[str], field: str) -> str | None return value +def _detect_source(tags: list[str]) -> str: + """Detect entry source from tags: webui, cli, auto-capture, or agent.""" + if "webui" in tags: + return "webui" + if "cli" in tags: + return "cli" + if "auto-capture" in tags: + return "auto" + return "agent" + + def _entry_to_dict(meta: dict, body: str, tier: str, *, preview: bool = True) -> dict: - """Convert store entry to JSON-serializable dict with v2.6 flags.""" + """Convert store entry to JSON-serializable dict with source flags.""" tags = meta.get("tags", []) or [] - is_auto = "auto-capture" in tags + source = _detect_source(tags) return { "id": meta.get("id", ""), "title": meta.get("title", ""), @@ -78,8 +90,9 @@ def _entry_to_dict(meta: dict, body: str, tier: str, *, preview: bool = True) -> "accessed": meta.get("accessed", ""), "access_count": meta.get("access_count", 0), "decay_score": meta.get("decay_score", 0), - "is_auto_capture": is_auto, - "is_manual": not is_auto, + "source": source, + "is_auto_capture": source == "auto", + "is_manual": source in ("webui", "cli"), "body_preview": (body[:200] + "…") if preview and len(body) > 200 else body, } @@ -164,11 +177,13 @@ def get_entry(request: Request, entry_id: str) -> dict: if "error" in result: return JSONResponse(status_code=404, content=result) - # Augment with v2.6 flags + # Augment with source flags meta = result.get("meta", {}) or {} tags = meta.get("tags", []) or [] - result["is_auto_capture"] = "auto-capture" in tags - result["is_manual"] = not result["is_auto_capture"] + source = _detect_source(tags) + result["source"] = source + result["is_auto_capture"] = source == "auto" + result["is_manual"] = source in ("webui", "cli") return result @@ -196,13 +211,18 @@ def create_entry(request: Request, payload: EntryCreate) -> dict: store = Store(root) store.recover() + # Tag entries created via WebUI so recall can distinguish source + tags = list(payload.tags) if payload.tags else [] + if "webui" not in tags: + tags.append("webui") + try: entry_id = store.write( body=payload.body, title=payload.title, entry_type=payload.type, scope=payload.scope, - tags=payload.tags or None, + tags=tags or None, project=payload.project, status=payload.status, priority=payload.priority, diff --git a/palaia/web/routes/search.py b/palaia/web/routes/search.py index d89184c..b05c7b0 100644 --- a/palaia/web/routes/search.py +++ b/palaia/web/routes/search.py @@ -78,11 +78,15 @@ def _run(): logger.error("BM25 fallback failed: %s", exc) result = {"results": [], "has_embeddings": False, "bm25_only": True} - # Augment results with manual/auto flags + # Augment results with source flags + from palaia.web.routes.entries import _detect_source + for r in result.get("results", []): tags = r.get("tags", []) or [] - r["is_auto_capture"] = "auto-capture" in tags - r["is_manual"] = not r["is_auto_capture"] + source = _detect_source(tags) + r["source"] = source + r["is_auto_capture"] = source == "auto" + r["is_manual"] = source in ("webui", "cli") return { "query": q, diff --git a/palaia/web/routes/status.py b/palaia/web/routes/status.py index b03bb1d..49d99a7 100644 --- a/palaia/web/routes/status.py +++ b/palaia/web/routes/status.py @@ -92,6 +92,23 @@ def list_agents(request: Request) -> dict: return {"agents": sorted(agents)} +@router.get("/tags") +def list_tags(request: Request) -> dict: + """List distinct tags found in entries.""" + from palaia.store import Store + + root = request.app.state.palaia_root + store = Store(root) + store.recover() + tags: set[str] = set() + for meta, _body, _tier in store.all_entries_unfiltered(include_cold=True): + for tag in meta.get("tags") or []: + tag = tag.strip() + if tag and tag not in ("auto-capture", "webui", "cli"): + tags.add(tag) + return {"tags": sorted(tags)} + + @router.get("/doctor") def run_doctor(request: Request) -> dict: """Run palaia doctor and return results for the UI banner. diff --git a/palaia/web/static/app.js b/palaia/web/static/app.js index 68ea577..6dcd89b 100644 --- a/palaia/web/static/app.js +++ b/palaia/web/static/app.js @@ -80,6 +80,50 @@ toast._timer = setTimeout(() => { t.className = "toast"; }, 3000); } + // ── Simple Markdown → HTML ──────────────────────────────────────────────── + function renderMarkdown(text) { + if (!text) return ""; + // Extract code blocks BEFORE escaping so their content stays raw. + const codeBlocks = []; + let safe = text.replace(/```(\w*)\n([\s\S]*?)```/g, (_m, _lang, code) => { + const idx = codeBlocks.length; + codeBlocks.push("
" + esc(code) + "
"); + return "\x00CB" + idx + "\x00"; + }); + // Extract inline code before escaping + const inlineCode = []; + safe = safe.replace(/`([^`]+)`/g, (_m, code) => { + const idx = inlineCode.length; + inlineCode.push("" + esc(code) + ""); + return "\x00IC" + idx + "\x00"; + }); + // Now escape remaining HTML + safe = esc(safe); + // Headings + safe = safe.replace(/^### (.+)$/gm, "

$1

"); + safe = safe.replace(/^## (.+)$/gm, "

$1

"); + safe = safe.replace(/^# (.+)$/gm, "

$1

"); + // Bold + italic + safe = safe.replace(/\*\*(.+?)\*\*/g, "$1"); + safe = safe.replace(/\*(.+?)\*/g, "$1"); + // Blockquotes + safe = safe.replace(/^> (.+)$/gm, "
$1
"); + // Unordered lists + safe = safe.replace(/^- (.+)$/gm, "
  • $1
  • "); + safe = safe.replace(/((?:
  • .*<\/li>\n?)+)/g, ""); + // Ordered lists + safe = safe.replace(/^(\d+)\. (.+)$/gm, "
  • $2
  • "); + safe = safe.replace(/((?:
  • .*<\/li>\n?)+)/g, (m) => m.includes("