From 471d8709f64a80a7aef0dd96cd03a981e1c67539 Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Fri, 27 Feb 2026 23:57:08 -0600 Subject: [PATCH] fix: support partial UUID prefix matching in memory/edge lookups Closes cal/claude-memory#3. All ID-based lookups now resolve partial UUID prefixes (git-style) instead of requiring exact full UUIDs. Affects _resolve_memory_path, _resolve_edge_path, edge_search, and related(). Ambiguous prefixes raise ValueError with candidates. Co-Authored-By: Claude Opus 4.6 --- skills/cognitive-memory/client.py | 49 +++++++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/skills/cognitive-memory/client.py b/skills/cognitive-memory/client.py index a30b6ff..5da0732 100644 --- a/skills/cognitive-memory/client.py +++ b/skills/cognitive-memory/client.py @@ -662,11 +662,32 @@ class CognitiveMemoryClient: content = f"{fm_str}\n\n{body.strip()}\n" if body.strip() else f"{fm_str}\n" path.write_text(content, encoding="utf-8") + @staticmethod + def _resolve_prefix(partial_id: str, keys) -> Optional[str]: + """Resolve a partial UUID prefix to a full ID (git-style). + + Returns the full key if exactly one match is found, None if zero + matches, or raises ValueError on ambiguous (multiple) matches. + """ + if partial_id in keys: + return partial_id + matches = [k for k in keys if k.startswith(partial_id)] + if len(matches) == 1: + return matches[0] + if len(matches) > 1: + raise ValueError( + f"Ambiguous ID prefix '{partial_id}' matches {len(matches)} " + f"entries: {', '.join(sorted(matches)[:5])}" + ) + return None + def _resolve_memory_path(self, memory_id: str) -> Optional[Path]: """Find the file path for a memory by ID using the index.""" index = self._load_index() - entry = index.get("entries", {}).get(memory_id) - if entry: + entries = index.get("entries", {}) + full_id = self._resolve_prefix(memory_id, entries) + if full_id: + entry = entries[full_id] path = self.memory_dir / entry["path"] if path.exists(): return path @@ -834,8 +855,10 @@ class CognitiveMemoryClient: def _resolve_edge_path(self, edge_id: str) -> Optional[Path]: """Find the file path for an edge by ID using the index.""" index = self._load_index() - entry = index.get("edges", {}).get(edge_id) - if entry: + edges = index.get("edges", {}) + full_id = self._resolve_prefix(edge_id, edges) + if full_id: + entry = edges[full_id] path = self.memory_dir / entry["path"] if path.exists(): return path @@ -1243,6 +1266,16 @@ class CognitiveMemoryClient: results = [] query_lower = query.lower().strip() if query else "" + # Resolve partial IDs to full UUIDs via prefix match + if from_id: + entries = index.get("entries", {}) + resolved = self._resolve_prefix(from_id, entries) + from_id = resolved or from_id + if to_id: + entries = index.get("entries", {}) + resolved = self._resolve_prefix(to_id, entries) + to_id = resolved or to_id + for eid, entry in index.get("edges", {}).items(): if types and entry.get("type") not in types: continue @@ -1532,12 +1565,18 @@ class CognitiveMemoryClient: visited = set() results = [] + # Resolve partial ID prefix to full UUID + entries = index.get("entries", {}) + resolved = self._resolve_prefix(memory_id, entries) + if resolved: + memory_id = resolved + def traverse(mid: str, depth: int): if depth > max_depth or mid in visited: return visited.add(mid) - entry = index.get("entries", {}).get(mid) + entry = entries.get(mid) if not entry: return