The embedding cache was keyed by skill_id alone, so any edit to a
SKILL.md body or description produced stale embeddings that
get_or_compute_embedding kept serving until a manual invalidate_cache
call or a file deletion. Previously this was mostly invisible because
select_skills_with_llm was the only caller exercising the cache; after
the preceding commit wires search_skills through the same path the
staleness becomes observable on every MCP query.
Use "{skill_id}:{sha256(embedding_text)[:16]}" as the cache key, so
any change to the text produced by _build_embedding_text (name +
description + body, truncated to SKILL_EMBEDDING_MAX_CHARS) causes
an automatic cache miss and a fresh embedding. Both
get_or_compute_embedding and _embedding_rank are updated.
Bounded growth: on each successful new compute, older entries with
the same "{skill_id}:" prefix are pruned in the same write. Net
result: at most one cached embedding per skill_id at any time, aside
from transient migration state.
Backward compatibility: existing pickle files keyed by skill_id alone
are migrated in place on first lookup (no API call needed); the old
key is dropped after migration.
invalidate_cache(skill_id) now removes every content-addressed entry
and any legacy entry for that skill_id, so historical versions do
not leak across evolutions.
Functional benchmark on a 28-skill local registry with
text-embedding-3-small via OpenRouter: top-1 match identity preserved
on all test queries, score drift below 0.001, warm latency
~260-400ms/query (unchanged from the previous commit).
Fixes#29. When a skill is already registered, register_skill_dir()
returned None, which caused fix_skill() to incorrectly report a failure.
Now returns the existing SkillMeta instead of None when the skill_id
is already present in the registry, making register_skill_dir() truly
idempotent as its callers (fix_skill, _auto_register_skill_dirs) expect.
Per reviewer feedback: keep strict \byes\b / \bno\b word boundaries to
prevent false positives, but widen confirm/reject/skip to stem-style
\bconfirm\w*\b etc. so common LLM variants like "confirmed", "rejected",
"skipping" still parse correctly instead of falling through to the
default False path.