diff --git a/clematis/memory/index.py b/clematis/memory/index.py index e780e04..9cc3cc5 100644 --- a/clematis/memory/index.py +++ b/clematis/memory/index.py @@ -12,6 +12,11 @@ def __init__(self) -> None: self._eps: List[Dict[str, Any]] = [] self._ver: int = 0 + def clear(self) -> None: + """Remove all episodes and reset version counter.""" + self._eps.clear() + self._ver = 0 + def add(self, ep: Dict[str, Any]) -> None: self._eps.append(ep) self._ver += 1 diff --git a/clematis/memory/lance_index.py b/clematis/memory/lance_index.py index cfe01c6..4608944 100644 --- a/clematis/memory/lance_index.py +++ b/clematis/memory/lance_index.py @@ -205,6 +205,44 @@ def add(self, ep: Dict[str, Any]) -> None: # Bump version counter self._bump_version() + def clear(self) -> None: + """Drop all stored episodes and reset the version counter.""" + # Drop the episodes table if it exists (ignore failures). + try: + drop = getattr(self._db, "drop_table", None) + if callable(drop) and self._episodes is not None: + drop(self._table_name) + except Exception: + # As a fallback, delete every row individually. + try: + rows = [row.get("id") for row in self._read_all_rows()] + for eid in rows: + if eid is None: + continue + try: + self._episodes.delete(f"id == '{eid}'") # type: ignore[union-attr] + except Exception: + pass + except Exception: + pass + finally: + self._episodes = None + + # Reset meta table back to the initial counter state. + try: + drop = getattr(self._db, "drop_table", None) + if callable(drop): + drop(self._meta_name) + except Exception: + try: + if self._meta is not None: + self._meta.delete("key == 'counter'") # type: ignore[union-attr] + except Exception: + pass + + self._meta = self._open_or_create_meta() + self._version_cache = 0 + def search_tiered( self, owner: Optional[str], diff --git a/healthcheck.md b/healthcheck.md index 8251df6..7dc704e 100644 --- a/healthcheck.md +++ b/healthcheck.md @@ -17,6 +17,7 @@ - Identity marker suite verified (`pytest -q -m identity`), and full test suite passes locally (`pytest -q`). - Built wheels into `dist_local/` and captured deterministic hashes via `shasum -a 256 dist_local/* | sort` as part of the packaging parity check. - Removed `ValueError` inheritance from typed errors, updated validators/scripts/tests to catch `ConfigError`/`SnapshotError`, and refreshed docs to match the cleaned taxonomy. +- Chat CLI `[wipe]` now purges the configured LanceDB store (when enabled) and reinitialises the memory index so subsequent `[seed]` runs repopulate the persistent backend with fresh embeddings. ## Recommended Next Actions - Re-run the identity and packaging matrices once the version/doc alignment is settled, ensuring the refreshed defaults keep byte-identical outputs. diff --git a/scripts/chat.py b/scripts/chat.py index be459b0..7a560cf 100644 --- a/scripts/chat.py +++ b/scripts/chat.py @@ -592,9 +592,17 @@ def _seed_memories( state["_chat_seeded_memories"] = True -def _wipe_memories(state: Dict[str, Any]) -> None: - state["mem_index"] = InMemoryIndex() - state["mem_backend"] = "inmemory" +def _wipe_memories(state: Dict[str, Any], cfg_t2: Dict[str, Any]) -> None: + idx = state.get("mem_index") + if idx is not None: + clear = getattr(idx, "clear", None) + if callable(clear): + try: + clear() + except Exception as exc: # pragma: no cover - defensive logging path + print(f"[chat] WARNING: failed to clear memory index: {exc}", file=sys.stderr) + state.pop("mem_index", None) + state.pop("mem_backend", None) state.pop("mem_backend_fallback_reason", None) state["_chat_seeded_memories"] = False state["_chat_memory_ids"] = [] @@ -613,6 +621,13 @@ def _wipe_memories(state: Dict[str, Any]) -> None: state["active_graphs"] = [DEFAULT_GRAPH_ID] state["_chat_seeded_graph"] = False _ensure_store(state) + # Recreate the configured index so subsequent seeds write to the persistent store. + try: + _ensure_index(state, cfg_t2) + except Exception as exc: # pragma: no cover - defensive logging + print(f"[chat] WARNING: failed to reinitialise memory index after wipe: {exc}", file=sys.stderr) + state["mem_index"] = InMemoryIndex() + state["mem_backend"] = "inmemory" def _apply_llm_mode(cfg: Dict[str, Any], args: argparse.Namespace) -> None: @@ -785,7 +800,7 @@ def main(argv: Optional[list[str]] = None) -> int: print("[reset] state reloaded.") continue if lowered in {"[wipe]", "/wipe"}: - _wipe_memories(state) + _wipe_memories(state, cfg_t2) print("[wipe] memories cleared.") if not args.no_seed: print(" (use [seed] to restore demo memories.)")