diff --git a/plugins/festinger/festinger/main.py b/plugins/festinger/festinger/main.py
index 727431d..4ac31a9 100644
--- a/plugins/festinger/festinger/main.py
+++ b/plugins/festinger/festinger/main.py
@@ -723,6 +723,24 @@ async def conflicts(request: Request) -> dict:
return {"conflicts": [format_row(r) for r in rows]}
+@app.post("/conflicts/clear")
+async def clear_conflicts(request: Request) -> dict:
+ """
+ Delete all pending conflicts from the resolution queue and clear the
+ in-memory pending set. Resolved/error rows are left untouched.
+ """
+ pool = request.app.state.pool
+ async with pool.acquire() as conn:
+ result = await conn.execute(
+ "DELETE FROM resolution_queue WHERE status = 'pending'"
+ )
+ # result is a string like "DELETE 17"
+ deleted = int(result.split()[-1]) if result else 0
+ cache.pending_conflicts.clear()
+ log.info("conflicts cleared deleted=%d", deleted)
+ return {"status": "ok", "deleted": deleted}
+
+
@app.get("/kg-log")
async def kg_log(request: Request, limit: int = 100, offset: int = 0, op: str = "") -> dict:
"""Return recent knowledge graph write log entries, newest first."""
@@ -1491,6 +1509,9 @@ ADMIN_HTML = """
Pending conflicts
+
+
+
Loading…
Knowledge graph write log
@@ -1549,6 +1570,23 @@ ADMIN_HTML = """
el.textContent = text;
}}
+ async function clearConflicts(btn) {{
+ if (!confirm('Delete all pending conflicts from the resolution queue?')) return;
+ btn.disabled = true;
+ showResult('Clearing pending conflicts…', true);
+ try {{
+ const r = await fetch('/conflicts/clear', {{method: 'POST'}});
+ const d = await r.json();
+ showResult(JSON.stringify(d, null, 2), r.ok);
+ await loadStats();
+ await loadConflicts();
+ }} catch(e) {{
+ showResult('Error: ' + e.message, false);
+ }} finally {{
+ btn.disabled = false;
+ }}
+ }}
+
async function runResolution(btn) {{
btn.disabled = true;
showResult('Running resolution job…', true);
diff --git a/plugins/festinger/festinger/recollection.py b/plugins/festinger/festinger/recollection.py
index 0b8a159..2049db4 100644
--- a/plugins/festinger/festinger/recollection.py
+++ b/plugins/festinger/festinger/recollection.py
@@ -17,11 +17,7 @@ from .cache import SoasRow, UrdEdge
log = logging.getLogger("festinger.recollection")
-ZERO_HIT_TEMPLATE = (
- "? {concept}: no recollection. If not a typo, store it before proceeding:\n"
- " gutask iknowthat '{concept} -isa in context of '\n"
- " gutask iknowthat '{concept} -ispart in context of '"
-)
+ZERO_HIT_TEMPLATE = "? {concept}: no recollection stored yet."
# ---------------------------------------------------------------------------
diff --git a/plugins/festinger/festinger/urd_writer.py b/plugins/festinger/festinger/urd_writer.py
index 6d8cead..67c4129 100644
--- a/plugins/festinger/festinger/urd_writer.py
+++ b/plugins/festinger/festinger/urd_writer.py
@@ -66,6 +66,15 @@ async def insert_urd_edge(
# Fast-path collision detection — in-memory
existing = cache.urd_by_concept_dim.get(key)
if existing is not None:
+ if existing.parent_id == req.parent_id:
+ # Identical re-assertion of an already-stored fact — silent no-op.
+ # This is normal: the cue scanner re-extracts the same facts from
+ # conversation history on every round.
+ log.debug(
+ "urd skip identical concept=%d parent=%d dim=%d",
+ req.concept_id, req.parent_id, req.dim_id,
+ )
+ return None
collision = CollisionInfo(
concept_id=req.concept_id,
existing_parent_id=existing.parent_id,