From 0ae26bda8d0a52e6159415d9d9c64799aece91f6 Mon Sep 17 00:00:00 2001 From: Arnold Wender Date: Wed, 22 Apr 2026 12:42:39 +0200 Subject: [PATCH 1/3] fix(encoding): replace non-ASCII symbols in CLI output (#1034) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces em-dashes (—), arrows (→), and box-drawing characters (─) in print() and logger.info() calls across 11 modules. These characters crash Windows terminals using GBK or CP1252 encoding with UnicodeEncodeError. Affected modules: repair, dedup, onboarding, entity_detector, convo_miner, layers, migrate, exporter, split_mega_files, mcp_server, room_detector_local. Replacements: — (U+2014 em-dash) -> -- (two ASCII hyphens) → (U+2192 right arrow) -> -> (ASCII arrow) ─ (U+2500 box-drawing dash) -> - (ASCII hyphen) Updates test_onboarding::test_hr_prints_line to match the new output. Closes #1034 (partial — remaining open() encoding fixes tracked separately) --- mempalace/convo_miner.py | 8 ++++---- mempalace/dedup.py | 4 ++-- mempalace/entity_detector.py | 6 +++--- mempalace/exporter.py | 2 +- mempalace/layers.py | 2 +- mempalace/mcp_server.py | 4 ++-- mempalace/migrate.py | 2 +- mempalace/onboarding.py | 8 ++++---- mempalace/repair.py | 8 ++++---- mempalace/room_detector_local.py | 6 +++--- mempalace/split_mega_files.py | 6 +++--- tests/test_onboarding.py | 2 +- 12 files changed, 29 insertions(+), 29 deletions(-) diff --git a/mempalace/convo_miner.py b/mempalace/convo_miner.py index 44d9f6a..d8647b4 100644 --- a/mempalace/convo_miner.py +++ b/mempalace/convo_miner.py @@ -889,7 +889,7 @@ def _mine_convos_impl( files = scan_convos(convo_dir) print(f"\n{'=' * 55}") - print(" MemPalace Mine — Conversations") + print(" MemPalace Mine -- Conversations") print(f"{'=' * 55}") print(f" Wing: {wing}") print(f" Source: {convo_path}") @@ -897,7 +897,7 @@ def _mine_convos_impl( print(f" Files: {len(files)}{limit_suffix}") print(f" Palace: {palace_path}") if dry_run: - print(" DRY RUN — nothing will be filed") + print(" DRY RUN -- nothing will be filed") print(f"{'-' * 55}\n") collection = _open_convo_collection( @@ -1018,9 +1018,9 @@ def _mine_convos_impl( type_counts = Counter(c.get("memory_type", "general") for c in chunks) types_str = ", ".join(f"{t}:{n}" for t, n in type_counts.most_common()) - print(f" [DRY RUN] {filepath.name} → {len(chunks)} memories ({types_str})") + print(f" [DRY RUN] {filepath.name} -> {len(chunks)} memories ({types_str})") else: - print(f" [DRY RUN] {filepath.name} → room:{room} ({len(chunks)} drawers)") + print(f" [DRY RUN] {filepath.name} -> room:{room} ({len(chunks)} drawers)") total_drawers += len(chunks) # Track room counts if extract_mode == "general": diff --git a/mempalace/dedup.py b/mempalace/dedup.py index 4c019d8..6b87c49 100644 --- a/mempalace/dedup.py +++ b/mempalace/dedup.py @@ -200,7 +200,7 @@ def dedup_palace( print(f" Drawers: {col.count():,}") print(f" Threshold: {threshold}") print(f" Mode: {'DRY RUN' if dry_run else 'LIVE'}") - print(f"{'─' * 55}") + print(f"{'-' * 55}") if wing: print(f" Wing: {wing}") @@ -227,7 +227,7 @@ def dedup_palace( elapsed = time.time() - t0 - print(f"\n{'─' * 55}") + print(f"\n{'-' * 55}") print(f" Done in {elapsed:.1f}s") print( f" Drawers: {total_kept + total_deleted:,} → {total_kept:,} (-{total_deleted:,} removed)" diff --git a/mempalace/entity_detector.py b/mempalace/entity_detector.py index df8968d..d199c32 100644 --- a/mempalace/entity_detector.py +++ b/mempalace/entity_detector.py @@ -768,7 +768,7 @@ def confirm_entities(detected: dict, yes: bool = False) -> dict: Pass yes=True to auto-accept all detected entities without prompting. """ print(f"\n{'=' * 58}") - print(" MemPalace — Entity Detection") + print(" MemPalace -- Entity Detection") print(f"{'=' * 58}") print("\n Scanned your files. Here's what we found:\n") @@ -798,7 +798,7 @@ def confirm_entities(detected: dict, yes: bool = False) -> dict: "topics": confirmed_topics, } - print(f"\n{'─' * 58}") + print(f"\n{'-' * 58}") print(" Options:") print(" [enter] Accept all") print(" [edit] Remove wrong entries or reclassify uncertain") @@ -813,7 +813,7 @@ def confirm_entities(detected: dict, yes: bool = False) -> dict: if choice == "edit": # Handle uncertain first if detected["uncertain"]: - print("\n Uncertain entities — classify each:") + print("\n Uncertain entities -- classify each:") for e in detected["uncertain"]: ans = input(f" {e['name']} — (p)erson, (r)project, or (s)kip? ").strip().lower() if ans == "p": diff --git a/mempalace/exporter.py b/mempalace/exporter.py index 2b874c6..01eef4f 100644 --- a/mempalace/exporter.py +++ b/mempalace/exporter.py @@ -84,7 +84,7 @@ def export_palace(palace_path: str, output_dir: str, format: str = "markdown") - total = col.count() if total == 0: - print(" Palace is empty — nothing to export.") + print(" Palace is empty -- nothing to export.") return {"wings": 0, "rooms": 0, "drawers": 0} _reject_symlink(output_dir, "output_dir") diff --git a/mempalace/layers.py b/mempalace/layers.py index b6135a8..c258a7e 100644 --- a/mempalace/layers.py +++ b/mempalace/layers.py @@ -469,7 +469,7 @@ if __name__ == "__main__": import json def usage(): - print("layers.py — 4-Layer Memory Stack") + print("layers.py -- 4-Layer Memory Stack") print() print("Usage:") print(" python layers.py wake-up Show L0 + L1") diff --git a/mempalace/mcp_server.py b/mempalace/mcp_server.py index 10f9b6a..f9131de 100644 --- a/mempalace/mcp_server.py +++ b/mempalace/mcp_server.py @@ -2985,7 +2985,7 @@ def tool_add_drawer( "The palace index may be stale; run reconnect or repair." ) _metadata_cache = None - logger.info(f"Filed drawer: {drawer_id} → {wing}/{room} ({len(chunk_ids)} chunks)") + logger.info(f"Filed drawer: {drawer_id} -> {wing}/{room} ({len(chunk_ids)} chunks)") return { "success": True, "drawer_id": drawer_id, @@ -3931,7 +3931,7 @@ def tool_diary_write(agent_name: str, entry: str, topic: str = "general", wing: } ) col.add(ids=chunk_ids, documents=chunk_docs, metadatas=chunk_metas) - logger.info(f"Diary entry: {entry_id} → {wing}/diary/{topic} ({len(chunk_ids)} chunks)") + logger.info(f"Diary entry: {entry_id} -> {wing}/diary/{topic} ({len(chunk_ids)} chunks)") return { "success": True, "entry_id": entry_id, diff --git a/mempalace/migrate.py b/mempalace/migrate.py index e25a586..1ccc527 100644 --- a/mempalace/migrate.py +++ b/mempalace/migrate.py @@ -305,7 +305,7 @@ def migrate(palace_path: str, dry_run: bool = False, confirm: bool = False): print(f" ROOM: {room:30} {count:5}") if dry_run: - print("\n DRY RUN — no changes made.") + print("\n DRY RUN -- no changes made.") print(f" Would migrate {len(drawers)} drawers.") return True diff --git a/mempalace/onboarding.py b/mempalace/onboarding.py index 427eb82..a31487a 100644 --- a/mempalace/onboarding.py +++ b/mempalace/onboarding.py @@ -57,7 +57,7 @@ DEFAULT_WINGS = { def _hr(): - print(f"\n{'─' * 58}") + print(f"\n{'-' * 58}") def _header(text): @@ -121,9 +121,9 @@ def _ask_mode() -> str: """) print(" How are you using MemPalace?") print() - print(" [1] Work — notes, projects, clients, colleagues, decisions") - print(" [2] Personal — diary, family, health, relationships, reflections") - print(" [3] Both — personal and professional mixed") + print(" [1] Work -- notes, projects, clients, colleagues, decisions") + print(" [2] Personal -- diary, family, health, relationships, reflections") + print(" [3] Both -- personal and professional mixed") print() while True: diff --git a/mempalace/repair.py b/mempalace/repair.py index 4ab67dc..a793eb6 100644 --- a/mempalace/repair.py +++ b/mempalace/repair.py @@ -531,7 +531,7 @@ def prune_corrupt(palace_path=None, confirm=False, collection_name: Optional[str bad_file = os.path.join(palace_path, "corrupt_ids.txt") if not os.path.exists(bad_file): - print(" No corrupt_ids.txt found — run scan first.") + print(" No corrupt_ids.txt found -- run scan first.") return with open(bad_file) as f: @@ -539,7 +539,7 @@ def prune_corrupt(palace_path=None, confirm=False, collection_name: Optional[str print(f" {len(bad_ids):,} corrupt IDs queued for deletion") if not confirm: - print("\n DRY RUN — no deletions performed.") + print("\n DRY RUN -- no deletions performed.") print(" Re-run with --confirm to actually delete.") return @@ -577,7 +577,7 @@ def prune_corrupt(palace_path=None, confirm=False, collection_name: Optional[str after = col.count() print(f"\n Deleted: {deleted:,}") print(f" Failed: {failed:,}") - print(f" Collection size: {before:,} → {after:,}") + print(f" Collection size: {before:,} -> {after:,}") # ChromaDB's ``collection.get()`` enforces an internal default ``limit`` @@ -1130,7 +1130,7 @@ def rebuild_index( return progress(f"\n{'=' * 55}") - progress(" MemPalace Repair — Index Rebuild") + progress(" MemPalace Repair -- Index Rebuild") progress(f"{'=' * 55}\n") progress(f" Palace: {palace_path}") diff --git a/mempalace/room_detector_local.py b/mempalace/room_detector_local.py index 8e3fc20..f754f46 100644 --- a/mempalace/room_detector_local.py +++ b/mempalace/room_detector_local.py @@ -232,14 +232,14 @@ def detect_rooms_from_files(project_dir: str) -> list: def print_proposed_structure(project_name: str, rooms: list, total_files: int, source: str): print(f"\n{'=' * 55}") - print(" MemPalace Init — Local setup") + print(" MemPalace Init -- Local setup") print(f"{'=' * 55}") print(f"\n WING: {project_name}") print(f" ({total_files} files found, rooms detected from {source})\n") for room in rooms: print(f" ROOM: {room['name']}") print(f" {room['description']}") - print(f"\n{'─' * 55}") + print(f"\n{'-' * 55}") def get_user_approval(rooms: list) -> list: @@ -259,7 +259,7 @@ def get_user_approval(rooms: list) -> list: if choice == "edit": print("\n Current rooms:") for i, room in enumerate(rooms): - print(f" {i + 1}. {room['name']} — {room['description']}") + print(f" {i + 1}. {room['name']} -- {room['description']}") remove = input("\n Room numbers to REMOVE (comma-separated, or enter to skip): ").strip() if remove: to_remove = {int(x.strip()) - 1 for x in remove.split(",") if x.strip().isdigit()} diff --git a/mempalace/split_mega_files.py b/mempalace/split_mega_files.py index c5b8d5d..8ce0c85 100644 --- a/mempalace/split_mega_files.py +++ b/mempalace/split_mega_files.py @@ -285,7 +285,7 @@ def main(): return print(f"\n{'=' * 60}") - print(f" Mega-file splitter — {'DRY RUN' if args.dry_run else 'SPLITTING'}") + print(f" Mega-file splitter -- {'DRY RUN' if args.dry_run else 'SPLITTING'}") print(f"{'=' * 60}") print(f" Source: {src_dir}") print(f" Output: {output_dir or 'same dir as source'}") @@ -307,9 +307,9 @@ def main(): print(f"{'-' * 60}") if args.dry_run: - print(f" DRY RUN — would create {total_written} files from {len(mega_files)} mega-files") + print(f" DRY RUN -- would create {total_written} files from {len(mega_files)} mega-files") else: - print(f" Done — created {total_written} files from {len(mega_files)} mega-files") + print(f" Done -- created {total_written} files from {len(mega_files)} mega-files") print() diff --git a/tests/test_onboarding.py b/tests/test_onboarding.py index 8a4f6c9..5d5da77 100644 --- a/tests/test_onboarding.py +++ b/tests/test_onboarding.py @@ -210,7 +210,7 @@ def test_generate_aaak_bootstrap_no_relationship(tmp_path): def test_hr_prints_line(capsys): _hr() out = capsys.readouterr().out - assert "─" in out + assert "-" in out def test_header_prints_banner(capsys): From 5aa0e11810b664b39d52904e7046e5041c23fc6b Mon Sep 17 00:00:00 2001 From: Arnold Wender Date: Fri, 1 May 2026 14:40:35 +0200 Subject: [PATCH 2/3] fix(encoding): replace non-ASCII chars in entity_detector output paths Replace em-dash in confirm_entities() input() prompt and filled/empty circle chars in confidence bar print() with ASCII equivalents. Both write to stdout and raise UnicodeEncodeError on Windows GBK/CP1252. --- mempalace/entity_detector.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mempalace/entity_detector.py b/mempalace/entity_detector.py index d199c32..3854f6d 100644 --- a/mempalace/entity_detector.py +++ b/mempalace/entity_detector.py @@ -748,7 +748,7 @@ def _print_entity_list(entities: list, label: str): print(" (none detected)") return for i, e in enumerate(entities): - confidence_bar = "●" * int(e["confidence"] * 5) + "○" * (5 - int(e["confidence"] * 5)) + confidence_bar = "#" * int(e["confidence"] * 5) + "." * (5 - int(e["confidence"] * 5)) signals_str = ", ".join(e["signals"][:2]) if e["signals"] else "" print(f" {i + 1:2}. {e['name']:20} [{confidence_bar}] {signals_str}") @@ -815,7 +815,7 @@ def confirm_entities(detected: dict, yes: bool = False) -> dict: if detected["uncertain"]: print("\n Uncertain entities -- classify each:") for e in detected["uncertain"]: - ans = input(f" {e['name']} — (p)erson, (r)project, or (s)kip? ").strip().lower() + ans = input(f" {e['name']} -- (p)erson, (r)project, or (s)kip? ").strip().lower() if ans == "p": confirmed_people.append(e["name"]) elif ans == "r": From 8c2856266fd1c2918eb79c0d7deabcea15f57280 Mon Sep 17 00:00:00 2001 From: Arnold Wender Date: Sat, 8 Aug 2026 22:29:59 +0200 Subject: [PATCH 3/3] fix(encoding): extend non-ASCII console sweep to the 11 remaining modules (#1034) The original sweep in this PR was scoped against develop as of April. Since then upstream added console output carrying the same characters, so the fix had drifted into covering roughly half of the affected surface. Re-measured against current develop and replaced the remaining occurrences of the same five characters this PR already targets -- U+2014 em-dash, U+2500 box drawing, U+2192 arrow, U+25CF/U+25CB circles -- on every print(), input() and logger call reachable from a terminal: repair.py (10), cli.py (7), format_miner.py (7), mcp_server.py (5), miner.py (2), onboarding.py (2), migrate.py (2), searcher.py (2), service.py (1), embedding.py (1), dialect.py (1) Substitutions match the ones already used in this branch: em-dash -> "--", box drawing -> "-", arrow -> "->", filled/empty circles -> "#"/".". Four assertions matched the previous literals and were updated alongside the strings they cover (test_cli, test_miner x2, test_repair). Docstrings, comments and stored content are deliberately untouched -- only what reaches a terminal, where GBK/CP1252 consoles raise UnicodeEncodeError. --- mempalace/cli.py | 14 +++++++------- mempalace/dialect.py | 2 +- mempalace/embedding.py | 2 +- mempalace/format_miner.py | 14 +++++++------- mempalace/mcp_server.py | 10 +++++----- mempalace/migrate.py | 4 ++-- mempalace/miner.py | 4 ++-- mempalace/onboarding.py | 4 ++-- mempalace/repair.py | 20 ++++++++++---------- mempalace/searcher.py | 4 ++-- mempalace/service.py | 2 +- tests/test_cli.py | 2 +- tests/test_miner.py | 4 ++-- tests/test_repair.py | 2 +- 14 files changed, 44 insertions(+), 44 deletions(-) diff --git a/mempalace/cli.py b/mempalace/cli.py index ee05f63..daeffde 100644 --- a/mempalace/cli.py +++ b/mempalace/cli.py @@ -166,7 +166,7 @@ def _run_pass_zero(project_dir, palace_dir, llm_provider) -> dict: samples = _gather_origin_samples(project_dir) if not samples: - print(" Skipping corpus-origin detection — no readable samples.") + print(" Skipping corpus-origin detection -- no readable samples.") return None # Tier 1 — always runs. Cheap regex grep, no API. @@ -431,7 +431,7 @@ def cmd_init(args): registry_path = add_to_known_entities(confirmed, wing=wing) print(f" Registry updated: {registry_path}") else: - print(" No entities detected — proceeding with directory-based rooms.") + print(" No entities detected -- proceeding with directory-based rooms.") # Pass 2: detect rooms from folder structure detect_rooms_local(project_dir=args.dir, yes=getattr(args, "yes", False)) @@ -878,7 +878,7 @@ def cmd_sync(args): project_dirs = project_dirs or None print(f"\n{'=' * 55}") - print(" MemPalace Sync — Gitignore-aware drawer prune") + print(" MemPalace Sync -- Gitignore-aware drawer prune") print(f"{'=' * 55}") print(f" Palace: {palace_path}") if args.wing: @@ -1183,7 +1183,7 @@ def cmd_hallways(args): config=MempalaceConfig(palace_path=palace_path), ) if not rows: - print("No hallways yet — they are built from drawer entities when you mine.") + print("No hallways yet -- they are built from drawer entities when you mine.") return rows.sort(key=lambda h: h.get("co_occurrence_count", 0), reverse=True) print(f" {len(rows)} hallway(s):") @@ -1651,7 +1651,7 @@ def cmd_repair(args): # withholds success until FTS5 rebuild, VACUUM, and quick_check are # clean. Its exception already includes the retained destination # and archive/source recovery paths. - print("\n Rebuild cleanup failed — see recovery details above.") + print("\n Rebuild cleanup failed -- see recovery details above.") sys.exit(1) # An empty counts dict is rebuild_from_sqlite's documented signal # for a validation refusal (missing source, existing dest, @@ -1985,12 +1985,12 @@ def cmd_serve(args): print(f" palace : {palace_path}") print(f" backend : {(backend or 'default').strip().lower() if backend else 'default'}") print(f" bind : {host}:{port} ({'loopback' if loopback else 'network-exposed'})") - print(f" tls : {'on' if tls_cert else 'off (plaintext — terminate TLS at a proxy)'}") + print(f" tls : {'on' if tls_cert else 'off (plaintext -- terminate TLS at a proxy)'}") print(f" read-only: {'yes' if args.read_only else 'no'}") if token_created: print("\n A new bearer token was generated and stored 0600 at:") print(f" {_server_token_path(palace_path)}") - print(" Store it securely — clients need it to connect:") + print(" Store it securely -- clients need it to connect:") print(f" {token}") print("\nConnect a client:") if token: diff --git a/mempalace/dialect.py b/mempalace/dialect.py index e6e214c..7b72746 100644 --- a/mempalace/dialect.py +++ b/mempalace/dialect.py @@ -1060,7 +1060,7 @@ if __name__ == "__main__": print("=== COMPRESSION STATS ===") print(f"JSON: ~{stats['original_tokens_est']:,} tokens (est)") print(f"AAAK: ~{stats['summary_tokens_est']:,} tokens (est)") - print(f"Ratio: {stats['size_ratio']}x (lossy — information is lost)") + print(f"Ratio: {stats['size_ratio']}x (lossy -- information is lost)") print() print("=== AAAK DIALECT OUTPUT ===") print(encoded) diff --git a/mempalace/embedding.py b/mempalace/embedding.py index 0e4ae74..2ad9c44 100644 --- a/mempalace/embedding.py +++ b/mempalace/embedding.py @@ -89,7 +89,7 @@ def _resolve_providers(device: str) -> tuple[list, str]: requested = _PROVIDER_MAP.get(device) if requested is None: if device not in _WARNED: - logger.warning("Unknown embedding_device %r — falling back to cpu", device) + logger.warning("Unknown embedding_device %r -- falling back to cpu", device) _WARNED.add(device) return (["CPUExecutionProvider"], "cpu") diff --git a/mempalace/format_miner.py b/mempalace/format_miner.py index a7800cc..3ec167d 100644 --- a/mempalace/format_miner.py +++ b/mempalace/format_miner.py @@ -362,7 +362,7 @@ def extract_text( logger.info("skip:unreadable (file gone after scan) %s", p) return None, ExtractionStatus.SKIP_UNREADABLE except OSError as exc: - logger.info("skip:unreadable %s — %s", p, exc) + logger.info("skip:unreadable %s -- %s", p, exc) return None, ExtractionStatus.SKIP_UNREADABLE # Fringe Case 5 — empty file. Skip silently. @@ -433,9 +433,9 @@ def extract_text( # Fringe Case 4 vs Case 10: encrypted vs generic crash, by message. msg = str(exc) if _ENCRYPTED_PATTERNS.search(msg): - logger.info("skip:encrypted %s — %s", p, msg[:120]) + logger.info("skip:encrypted %s -- %s", p, msg[:120]) return None, ExtractionStatus.SKIP_ENCRYPTED - logger.warning("skip:extraction_error %s — %s: %s", p, type(exc).__name__, msg[:200]) + logger.warning("skip:extraction_error %s -- %s: %s", p, type(exc).__name__, msg[:200]) return None, ExtractionStatus.SKIP_EXTRACTION_ERROR # Either transformer can legitimately return None / empty (malformed @@ -443,7 +443,7 @@ def extract_text( # so the caller knows to skip rather than file an empty drawer. if not text: transformer = "striprtf" if is_rtf else "markitdown" - logger.info("skip:extraction_error %s — %s returned None/empty", p, transformer) + logger.info("skip:extraction_error %s -- %s returned None/empty", p, transformer) return None, ExtractionStatus.SKIP_EXTRACTION_ERROR return text, ExtractionStatus.OK @@ -803,7 +803,7 @@ def mine_formats( files = scan_formats(format_path) print(f"\n{'=' * 55}") - print(" MemPalace Mine — Format extraction") + print(" MemPalace Mine -- Format extraction") print(f"{'=' * 55}") print(f" Wing: {wing}") print(f" Source: {format_path}") @@ -811,7 +811,7 @@ def mine_formats( print(f" Files: {len(files)}{limit_suffix}") print(f" Palace: {palace_path}") if dry_run: - print(" DRY RUN — nothing will be filed") + print(" DRY RUN -- nothing will be filed") print(f"{'-' * 55}\n") collection = get_collection(palace_path) if not dry_run else None @@ -876,7 +876,7 @@ def mine_formats( files_with_text += 1 if dry_run: - print(f" [DRY RUN] {filepath.name} → {len(chunks)} drawers") + print(f" [DRY RUN] {filepath.name} -> {len(chunks)} drawers") total_drawers += len(chunks) files_mined += 1 if limit > 0 and files_mined >= limit: diff --git a/mempalace/mcp_server.py b/mempalace/mcp_server.py index f9131de..5369ce0 100644 --- a/mempalace/mcp_server.py +++ b/mempalace/mcp_server.py @@ -2950,7 +2950,7 @@ def tool_add_drawer( "The palace index may be stale; run reconnect or repair." ) _metadata_cache = None - logger.info(f"Filed drawer: {drawer_id} → {wing}/{room}") + logger.info(f"Filed drawer: {drawer_id} -> {wing}/{room}") return { "success": True, "drawer_id": drawer_id, @@ -3883,7 +3883,7 @@ def tool_diary_write(agent_name: str, entry: str, topic: str = "general", wing: documents=[entry], metadatas=[{**base_metadata, "chunk_index": 0}], ) - logger.info(f"Diary entry: {entry_id} → {wing}/diary/{topic}") + logger.info(f"Diary entry: {entry_id} -> {wing}/diary/{topic}") return { "success": True, "entry_id": entry_id, @@ -7294,10 +7294,10 @@ def _run_stdio_loop() -> None: # clean EOF — same meaning: the client is gone. Never loop on # it: an orphaned stdio server holding the mine_palace flock # blocked all palace writes for hours (2026-07-10 outage). - logger.info("stdin read failed (%s) — client disconnected, shutting down", exc) + logger.info("stdin read failed (%s) -- client disconnected, shutting down", exc) break if not line: - logger.info("stdin EOF — client disconnected, shutting down") + logger.info("stdin EOF -- client disconnected, shutting down") break line = line.strip() @@ -7327,7 +7327,7 @@ def _run_stdio_loop() -> None: # The client's read end is gone; every future response write # would fail the same way, so treat it like stdin EOF and # shut down instead of swallowing it in the generic handler. - logger.info("stdout write failed (%s) — client disconnected, shutting down", exc) + logger.info("stdout write failed (%s) -- client disconnected, shutting down", exc) _drop_broken_stdout() break diff --git a/mempalace/migrate.py b/mempalace/migrate.py index 1ccc527..30f232d 100644 --- a/mempalace/migrate.py +++ b/mempalace/migrate.py @@ -570,7 +570,7 @@ def migrate_wing_names(palace_path: str, dry_run: bool = False, confirm: bool = topic_renames = _plan_topics_by_wing_renames() if not d_updates and not c_updates and not topic_renames: - print(" All wing names are already normalized — nothing to migrate.") + print(" All wing names are already normalized -- nothing to migrate.") return False print("\n Wing-name migration plan:") @@ -586,7 +586,7 @@ def migrate_wing_names(palace_path: str, dry_run: bool = False, confirm: bool = print(f" topics_by_wing: {len(topic_renames)} key(s) re-keyed") if dry_run: - print("\n DRY RUN — no changes made.\n") + print("\n DRY RUN -- no changes made.\n") return True if not confirm: diff --git a/mempalace/miner.py b/mempalace/miner.py index 80fc615..6915fe6 100644 --- a/mempalace/miner.py +++ b/mempalace/miner.py @@ -1843,7 +1843,7 @@ def _mine_impl( print(f" Palace: {palace_path}") print(f" Device: {describe_device()}") if dry_run: - print(" DRY RUN — nothing will be filed") + print(" DRY RUN -- nothing will be filed") if not respect_gitignore: print(" .gitignore: DISABLED") if include_ignored: @@ -2183,7 +2183,7 @@ def status(palace_path: str): def _print_status(total: int, wing_rooms: dict[str, dict[str, int]]) -> None: """Render the wing/room histogram shared by both status code paths.""" print(f"\n{'=' * 55}") - print(f" MemPalace Status — {total} drawers") + print(f" MemPalace Status -- {total} drawers") print(f"{'=' * 55}\n") for wing, rooms in sorted(wing_rooms.items()): print(f" WING: {wing}") diff --git a/mempalace/onboarding.py b/mempalace/onboarding.py index a31487a..43e65f0 100644 --- a/mempalace/onboarding.py +++ b/mempalace/onboarding.py @@ -430,7 +430,7 @@ def run_onboarding( print() if _yn(" Add any of these to your registry?"): for e in detected: - ans = input(f" {e['name']} — (p)erson, (s)kip? ").strip().lower() + ans = input(f" {e['name']} -- (p)erson, (s)kip? ").strip().lower() if ans == "p": rel = input(f" Relationship/role for {e['name']}? ").strip() ctx = ( @@ -439,7 +439,7 @@ def run_onboarding( else ( "work" if mode == "work" - else input(" Context — (p)ersonal or (w)ork? ") + else input(" Context -- (p)ersonal or (w)ork? ") .strip() .lower() .replace("w", "work") diff --git a/mempalace/repair.py b/mempalace/repair.py index a793eb6..3be7389 100644 --- a/mempalace/repair.py +++ b/mempalace/repair.py @@ -1646,7 +1646,7 @@ def rebuild_from_sqlite( in_place = source_palace == dest_palace print(f"\n{'=' * 55}") - print(" MemPalace Repair — Rebuild from SQLite") + print(" MemPalace Repair -- Rebuild from SQLite") print(f"{'=' * 55}\n") print(f" Source: {source_palace}") print(f" Dest: {dest_palace}") @@ -1741,7 +1741,7 @@ def _preview_rebuild_from_sqlite( Never archives, locks, or writes. Returns ``{}`` if SQLite counts are unreadable so a broken preview cannot look like a successful zero-row plan. """ - print("\n DRY RUN — no changes will be made.") + print("\n DRY RUN -- no changes will be made.") if in_place: print( f" Would archive {dest_palace} → " @@ -1787,7 +1787,7 @@ def _preview_legacy_repair( Returns ``{}`` when the count is unreadable so a broken preview cannot look like a valid plan (#1654, #2095, #2133). """ - print("\n DRY RUN — no changes will be made.") + print("\n DRY RUN -- no changes will be made.") n = sqlite_drawer_count(palace_path, collection_name) if n is None: _print_unreadable_count_refusal(collection_name=collection_name, palace_path=palace_path) @@ -1822,8 +1822,8 @@ def _preview_legacy_repair( f" returns fewer than {n} (#1208 truncation guard). It would then, in order:" ) if os.path.exists(backup_path): - print(f" 1. DELETE the existing backup at {backup_path} — or refuse outright") - print(" if it is not a palace — and copy the live palace in its place") + print(f" 1. DELETE the existing backup at {backup_path} -- or refuse outright") + print(" if it is not a palace -- and copy the live palace in its place") else: print(f" 1. copy the palace directory to {backup_path}") print(f" 2. DELETE the live '{collection_name}' collection and re-file the extracted rows") @@ -1895,7 +1895,7 @@ def _rebuild_from_sqlite_locked( if in_place: ts = datetime.now().strftime("%Y%m%d-%H%M%S") archive_path = f"{dest_palace}.pre-rebuild-{ts}" - print(f" Archiving {dest_palace} → {archive_path}") + print(f" Archiving {dest_palace} -> {archive_path}") # os.rename, NOT shutil.move. When any file inside the palace is # held open by another process (MCP server, a running mine, another # harness), renaming the directory fails atomically UP FRONT on @@ -2024,7 +2024,7 @@ def status(palace_path=None, collection_name: Optional[str] = None) -> dict: palace_path = palace_path or _get_palace_path() collection_name = collection_name or _drawers_collection_name() print(f"\n{'=' * 55}") - print(" MemPalace Repair — Status") + print(" MemPalace Repair -- Status") print(f"{'=' * 55}\n") print(f" Palace: {palace_path}") @@ -2241,7 +2241,7 @@ def repair_max_seq_id( } print(f"\n{'=' * 55}") - print(" MemPalace Repair — max_seq_id Un-poison") + print(" MemPalace Repair -- max_seq_id Un-poison") print(f"{'=' * 55}\n") print(f" Palace: {palace_path}") if segment: @@ -2292,10 +2292,10 @@ def repair_max_seq_id( source = "sidecar" if from_sidecar else "heuristic (collection MAX)" print(f" clean-value source {source}") for seg_id, old_val, new_val in plan: - print(f" {seg_id} {old_val} → {new_val}") + print(f" {seg_id} {old_val} -> {new_val}") if dry_run: - print("\n DRY RUN — no rows modified.\n" + "=" * 55 + "\n") + print("\n DRY RUN -- no rows modified.\n" + "=" * 55 + "\n") return result if not plan: diff --git a/mempalace/searcher.py b/mempalace/searcher.py index 71f139f..205569b 100644 --- a/mempalace/searcher.py +++ b/mempalace/searcher.py @@ -585,7 +585,7 @@ def _print_search_results_bm25_only( for line in (hit.get("text", "") or "").strip().split("\n"): print(f" {line}") print() - print(f" {'─' * 56}") + print(f" {'-' * 56}") print() @@ -692,7 +692,7 @@ def search(query: str, palace_path: str, wing: str = None, room: str = None, n_r for line in hit["text"].strip().split("\n"): print(f" {line}") print() - print(f" {'─' * 56}") + print(f" {'-' * 56}") print() diff --git a/mempalace/service.py b/mempalace/service.py index b4ec76b..39983e1 100644 --- a/mempalace/service.py +++ b/mempalace/service.py @@ -273,7 +273,7 @@ def run_sync(payload: dict[str, Any]) -> dict[str, Any]: dry_run = bool(payload.get("dry_run", True)) print(f"\n{'=' * 55}") - print(" MemPalace Sync — Gitignore-aware drawer prune") + print(" MemPalace Sync -- Gitignore-aware drawer prune") print(f"{'=' * 55}") print(f" Palace: {palace_path}") if payload.get("wing"): diff --git a/tests/test_cli.py b/tests/test_cli.py index a190c94..fd3e763 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -1246,7 +1246,7 @@ def test_cmd_repair_default_mode_dry_run_writes_nothing(mock_config_cls, tmp_pat cmd_repair(args) out = capsys.readouterr().out - assert "DRY RUN — no changes will be made." in out + assert "DRY RUN -- no changes will be made." in out assert "holds 2 rows" in out assert "Repair complete" not in out # The count comes from read-only SQLite, never from a chromadb client: diff --git a/tests/test_miner.py b/tests/test_miner.py index b22c394..83efc7c 100644 --- a/tests/test_miner.py +++ b/tests/test_miner.py @@ -1093,7 +1093,7 @@ def test_status_does_not_cold_load_vector_index(palace_path, seeded_collection, sentinel.assert_not_called() out = capsys.readouterr().out - assert "MemPalace Status — 4 drawers" in out + assert "MemPalace Status -- 4 drawers" in out assert "WING: project" in out assert "WING: notes" in out @@ -1128,7 +1128,7 @@ def test_status_falls_back_to_chroma_when_sqlite_unreadable(palace_path, seeded_ status(palace_path) out = capsys.readouterr().out - assert "MemPalace Status — 4 drawers" in out + assert "MemPalace Status -- 4 drawers" in out assert "WING: project" in out diff --git a/tests/test_repair.py b/tests/test_repair.py index 60fce21..2179e84 100644 --- a/tests/test_repair.py +++ b/tests/test_repair.py @@ -2481,7 +2481,7 @@ def test_cmd_repair_dry_run_leaves_a_real_palace_byte_identical(tmp_path, capsys out = capsys.readouterr().out assert snapshot() == before assert not (tmp_path / "palace.backup").exists() - assert "DRY RUN — no changes will be made." in out + assert "DRY RUN -- no changes will be made." in out assert "holds 4 rows" in out assert "Repair complete" not in out