Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions packages/openclaw-plugin/src/hooks/recall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 || "",
Expand Down
7 changes: 5 additions & 2 deletions palaia/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
42 changes: 31 additions & 11 deletions palaia/web/routes/entries.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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", ""),
Expand All @@ -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,
}

Expand Down Expand Up @@ -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


Expand Down Expand Up @@ -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,
Expand Down
10 changes: 7 additions & 3 deletions palaia/web/routes/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
17 changes: 17 additions & 0 deletions palaia/web/routes/status.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
88 changes: 79 additions & 9 deletions palaia/web/static/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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("<pre>" + esc(code) + "</pre>");
return "\x00CB" + idx + "\x00";
});
// Extract inline code before escaping
const inlineCode = [];
safe = safe.replace(/`([^`]+)`/g, (_m, code) => {
const idx = inlineCode.length;
inlineCode.push("<code>" + esc(code) + "</code>");
return "\x00IC" + idx + "\x00";
});
// Now escape remaining HTML
safe = esc(safe);
// Headings
safe = safe.replace(/^### (.+)$/gm, "<h3>$1</h3>");
safe = safe.replace(/^## (.+)$/gm, "<h2>$1</h2>");
safe = safe.replace(/^# (.+)$/gm, "<h1>$1</h1>");
// Bold + italic
safe = safe.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>");
safe = safe.replace(/\*(.+?)\*/g, "<em>$1</em>");
// Blockquotes
safe = safe.replace(/^&gt; (.+)$/gm, "<blockquote>$1</blockquote>");
// Unordered lists
safe = safe.replace(/^- (.+)$/gm, "<li>$1</li>");
safe = safe.replace(/((?:<li>.*<\/li>\n?)+)/g, "<ul>$1</ul>");
// Ordered lists
safe = safe.replace(/^(\d+)\. (.+)$/gm, "<li>$2</li>");
safe = safe.replace(/((?:<li>.*<\/li>\n?)+)/g, (m) => m.includes("<ul>") ? m : "<ol>" + m + "</ol>");
// Paragraphs (double newline)
safe = safe.replace(/\n\n/g, "</p><p>");
// Single newlines → <br>
safe = safe.replace(/\n/g, "<br>");
// Restore code blocks and inline code
safe = safe.replace(/\x00CB(\d+)\x00/g, (_m, idx) => codeBlocks[idx]);
safe = safe.replace(/\x00IC(\d+)\x00/g, (_m, idx) => inlineCode[idx]);
return "<p>" + safe + "</p>";
}

// ── Date formatting ──────────────────────────────────────────────────────
function fmtDate(iso) {
if (!iso) return "—";
Expand All @@ -103,8 +147,10 @@
try {
const d = await apiGet("/api/projects");
const sel = $("filter-project");
const dl = $("project-options");
for (const name of Object.keys(d.projects || {})) {
sel.appendChild(el("option", { value: name }, name));
dl.appendChild(el("option", { value: name }));
}
} catch (e) { /* non-fatal */ }
}
Expand All @@ -113,8 +159,20 @@
try {
const d = await apiGet("/api/agents");
const sel = $("filter-agent");
const dl = $("agent-options");
for (const name of d.agents || []) {
sel.appendChild(el("option", { value: name }, name));
dl.appendChild(el("option", { value: name }));
}
} catch (e) { /* non-fatal */ }
}

async function loadTags() {
try {
const d = await apiGet("/api/tags");
const dl = $("tag-options");
for (const tag of d.tags || []) {
dl.appendChild(el("option", { value: tag }));
}
} catch (e) { /* non-fatal */ }
}
Expand Down Expand Up @@ -196,7 +254,7 @@
id: r.id, title: r.title, type: r.type, scope: r.scope, tier: r.tier,
tags: r.tags || [], project: r.project, status: r.status, priority: r.priority,
decay_score: r.decay_score, body_preview: r.body || r.content,
score: r.score, is_manual: r.is_manual, is_auto_capture: r.is_auto_capture,
score: r.score, source: r.source, is_manual: r.is_manual, is_auto_capture: r.is_auto_capture,
agent: r.agent,
}));
state.entries = entries;
Expand Down Expand Up @@ -261,15 +319,17 @@
const meta = el("div", { class: "entry-meta" });
meta.appendChild(el("span", { class: "badge badge-tier badge-" + (e.tier || "hot") }, e.tier || "hot"));
meta.appendChild(el("span", { class: "badge badge-type badge-" + (e.type || "memory") }, e.type || "memory"));
meta.appendChild(el("span", { class: "badge badge-source " + (e.is_manual ? "badge-manual" : "badge-auto") },
e.is_manual ? "manual ✦" : "auto"));
const sourceLabel = { webui: "webui", cli: "cli", auto: "auto", agent: "agent" }[e.source] || "auto";
const sourceBadge = e.is_manual ? "badge-manual" : (e.source === "agent" ? "badge-agent-source" : "badge-auto");
meta.appendChild(el("span", { class: "badge badge-source " + sourceBadge }, sourceLabel));
if (e.priority) meta.appendChild(el("span", { class: "badge badge-priority-" + e.priority }, e.priority));
if (e.status) meta.appendChild(el("span", { class: "badge badge-status" }, e.status));
if (e.scope && e.scope !== "team") meta.appendChild(el("span", { class: "badge badge-scope" }, e.scope));
if (e.project) meta.appendChild(el("span", { class: "badge badge-project" }, e.project));
if (e.agent) meta.appendChild(el("span", { class: "badge badge-agent" }, "@" + e.agent));
const sourceTags = new Set(["auto-capture", "webui", "cli"]);
for (const tag of (e.tags || []).slice(0, 4)) {
if (tag === "auto-capture") continue; // already shown via source badge
if (sourceTags.has(tag)) continue; // already shown via source badge
meta.appendChild(el("span", { class: "tag" }, tag));
}
card.appendChild(meta);
Expand Down Expand Up @@ -313,17 +373,25 @@

function renderDetailPanel(id, d) {
const m = d.meta || {};
const isManual = d.is_manual ?? !(m.tags || []).includes("auto-capture");
const source = d.source || (d.is_manual ? "cli" : "auto");
const sourceLabels = {
webui: "webui (1.3× boost)",
cli: "cli (1.3× boost)",
auto: "auto-capture",
agent: "agent",
};
const sourceTags = new Set(["auto-capture", "webui", "cli"]);
const displayTags = (m.tags || []).filter(t => !sourceTags.has(t));

const rows = [
["Type", m.type || "memory"],
["Scope", m.scope || "team"],
["Tier", m.tier || "—"],
["Source", isManual ? "manual (1.3× boost)" : "auto-capture"],
["Source", sourceLabels[source] || source],
["Created", fmtDate(m.created)],
["Accessed", fmtDate(m.accessed)],
["Decay", Number(m.decay_score || 0).toFixed(4)],
["Tags", (m.tags || []).join(", ") || "—"],
["Tags", displayTags.join(", ") || "—"],
["Agent", m.agent || "—"],
];
if (m.priority) rows.push(["Priority", m.priority]);
Expand All @@ -347,7 +415,8 @@
),
);

const body = el("pre", { class: "detail-body" }, d.content || "");
const body = el("div", { class: "detail-body" });
body.innerHTML = renderMarkdown(d.content || "");

return el("div", { id: "inline-detail-" + id, class: "inline-detail" }, toolbar, dl, body);
}
Expand Down Expand Up @@ -443,7 +512,7 @@
$("form-type").value = m.type || "memory";
$("form-scope").value = m.scope || "team";
$("form-project").value = m.project || "";
$("form-tags").value = (m.tags || []).filter(t => t !== "auto-capture").join(", ");
$("form-tags").value = (m.tags || []).filter(t => !["auto-capture", "webui", "cli"].includes(t)).join(", ");
$("form-agent").value = m.agent || "";
$("form-priority").value = m.priority || "";
$("form-status").value = m.status || "";
Expand Down Expand Up @@ -598,6 +667,7 @@
loadStatus();
loadProjects();
loadAgents();
loadTags();
loadDoctor();
loadEntries();
loadTasks();
Expand Down
Loading
Loading