Skip to content

Task-agnostic entity conflict resolution may cause incorrect dedup across tasks when applied to guideline entities #301

Description

@evduester

Root cause

resolve_conflicts() (altk_evolve/llm/conflict_resolution/conflict_resolution.py) decides ADD/UPDATE/DELETE/NONE by sending old vs. new entities to an LLM as SimpleEntity objects (altk_evolve/schema/conflict_resolution.py), which carry only id / type / content. task_description — and every other structured field (trigger, rationale, implementation_steps, category) — lives only in metadata, which is stripped before the LLM ever sees it.

The old-entity candidate pool itself is also task-agnostic: for each new guideline, update_entities() (altk_evolve/backend/base.py) runs a content-similarity search (_search_entities_impl, filters={"type": "guideline"}, limit=10) with no task_description scoping at all.

Net effect: the LLM's dedup decision is based purely on whether two guidelines read the same, with no way to know whether they came from the same task or unrelated ones.

Failure mode 1a — identical/paraphrased content across different tasks → NONE

When a new guideline (from task A) reads as a paraphrase of an existing stored guideline (originally learned from a different task), the prompt's "No Change" rule fires. Per the prompt's own few-shot example, a NONE verdict for a retrieved entity means that entity is dropped from the LLM's response entirely — not echoed back with event: "NONE", just absent. Nothing in resolve_conflicts() or the
base.py update loop creates any record for it.

Lost as a result:

  • Task A's task_description, rationale, trigger, implementation_steps, source_task_id for this occurrence — never persisted anywhere.
  • support is not incremented (confirmed: nothing in resolve_conflicts() or the NONE/absent-entity path touches support; it's only ever recomputed by the separate, manually-triggered consolidate_guidelines()combine_cluster()_attribute_support() pipeline in altk_evolve/llm/guidelines/clustering.py).
  • Because support doesn't move, this reconfirmation never nudges the guideline toward core_support promotion in select_guidelines() (altk_evolve/llm/guidelines/retrieval.py) — the "recurred across many tasks, so it generalizes" signal the core mechanism is meant to capture is silently discarded here.
  • In the dosage-aware retrieval path's default similarity_key="source_task", the stored entity's retrieval ranking for future task-A-shaped queries stays anchored to the original task's task_description — it's never reinforced toward matching task A-like queries, even though this occurrence just proved the advice applies there too.

Note: the stored entity itself isn't deleted or made unreachable — it can still surface for task A via content-similarity search (static injection path) or if it's already core. What's lost is the reinforcement signal and task-A-specific provenance, not necessarily all future injectability.

Failure mode 1b — similar-but-not-identical content across different tasks → UPDATE

When the LLM instead judges task A's version as adding "concrete new information" (rule 2, UPDATE), the metadata merge in resolve_conflicts() (conflict_resolution.py, ~line 55-78) does:

merged = {**stored_metadata, **incoming_metadata}

Every key except the sticky generation_method
(_STICKY_STORED_METADATA_KEYS) is overwritten by the incoming (task A) values — including task_description, rationale, category, trigger, implementation_steps, source_task_id. content is also replaced outright with task A's phrasing (that's what the LLM returned as the UPDATE's content).

Result: the original task's provenance is gone from the stored entity, and the canonical version of this guideline is now phrased/scoped to task A's specifics — which may not actually generalize back to the original task. Two distinct tasks' learnings get collapsed into one entity that only accurately represents the more recent one.

Why "just increment support on NONE" isn't the fix

Considered and rejected as too blunt:

  • The NONE verdict is an approximate, per-call LLM judgment over a top-10 content-similarity candidate window — not a verified duplicate check. Turning it into a persistent counter would make support (and therefore core_support promotion) inherit that noise/non-determinism.
  • It risks unbounded growth tied to how often a task type runs, not to how broadly useful the advice actually is — the opposite of what core_support should measure.
  • It wouldn't recover the actual lost information (task A's task_description, trigger, rationale, etc.) — just bumps a number.
  • support accounting today is deliberately confined to the auditable consolidate_guidelines() pipeline, which conserves total support and warns on mismatch (_attribute_support`). Silently mutating it from inside the write-time dedup path would bypass that discipline.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions