From 6f1c6a73edff177c2f011e605d4076a350a1f063 Mon Sep 17 00:00:00 2001 From: Ismael Marechalle Date: Thu, 2 Jul 2026 15:30:39 +0200 Subject: [PATCH 1/3] engine: fix UnicodeEncodeError in brain_search on Windows Windows consoles default stdout/stderr to the system codepage (e.g. cp1252), which can't encode em-dashes/arrows/accents present in vault snippets and crashes brain_search.py query with UnicodeEncodeError. Force UTF-8 on both streams at startup so queries never crash. Found via a real-Windows smoke test of install.ps1. --- engine/search/brain_search.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/engine/search/brain_search.py b/engine/search/brain_search.py index ecd21ca..b4ef725 100644 --- a/engine/search/brain_search.py +++ b/engine/search/brain_search.py @@ -23,6 +23,13 @@ import unicodedata from pathlib import Path +# Windows consoles default stdout/stderr to the system codepage (e.g. cp1252), +# which can't encode arrows/em-dashes/accents found in vault snippets and +# crashes with UnicodeEncodeError. Force UTF-8 so query output never crashes. +for _stream in (sys.stdout, sys.stderr): + if hasattr(_stream, "reconfigure"): + _stream.reconfigure(encoding="utf-8") + BRAIN = Path(os.environ.get("BRAIN_DIR") or (Path.home() / "Documents" / "Brain")) INDEX = Path(os.environ.get("BRAIN_SEARCH_INDEX") or (Path.home() / ".brain-search" / "index.json")) K1, B = 1.5, 0.75 From 1848f4a40191215c35a2514ecff39c11a92efd3d Mon Sep 17 00:00:00 2001 From: Ismael Marechalle Date: Fri, 3 Jul 2026 17:03:21 +0200 Subject: [PATCH 2/3] engine: fix UTF-8 file I/O in session hooks on Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same root cause as the brain_search.py fix: session-recap.py, daily-reflection.py, session-indexer.py, and session-logger.py all use Path.read_text()/write_text()/open() without encoding="utf-8", so Windows defaults to the system codepage (cp1252). This crashed session-recap.py on every single Stop event (UnicodeEncodeError on the 📊 emoji it writes, UnicodeDecodeError reading UTF-8 transcript files), silently breaking the Journal/ auto-summary feature end-to-end on Windows since install. Found while manually verifying the brain-in-a-box install actually captures session data as advertised. --- engine/hooks/daily-reflection.py | 6 +++--- engine/hooks/session-indexer.py | 2 +- engine/hooks/session-logger.py | 2 +- engine/hooks/session-recap.py | 14 +++++++------- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/engine/hooks/daily-reflection.py b/engine/hooks/daily-reflection.py index 3df269e..9a014c6 100755 --- a/engine/hooks/daily-reflection.py +++ b/engine/hooks/daily-reflection.py @@ -17,7 +17,7 @@ sys.exit(0) sessions = [] -for line in index_file.read_text().splitlines(): +for line in index_file.read_text(encoding="utf-8").splitlines(): try: sessions.append(json.loads(line)) except Exception: @@ -30,7 +30,7 @@ tp = s.get("transcript_path") if tp and Path(tp).exists(): try: - transcripts.append(Path(tp).read_text()[:50000]) + transcripts.append(Path(tp).read_text(encoding="utf-8")[:50000]) except Exception: pass @@ -69,5 +69,5 @@ check=False, ) except Exception as e: - (LOGS / "daily-reflection-errors.log").open("a").write(f"{time.strftime(chr(37)+chr(70)+chr(84)+chr(37)+chr(84))} {e}" + chr(10)) + (LOGS / "daily-reflection-errors.log").open("a", encoding="utf-8").write(f"{time.strftime(chr(37)+chr(70)+chr(84)+chr(37)+chr(84))} {e}" + chr(10)) sys.exit(0) diff --git a/engine/hooks/session-indexer.py b/engine/hooks/session-indexer.py index 86bb753..9223e89 100755 --- a/engine/hooks/session-indexer.py +++ b/engine/hooks/session-indexer.py @@ -24,6 +24,6 @@ "cwd": payload.get("cwd") or os.getcwd(), "transcript_path": payload.get("transcript_path"), } -with (log_dir / f"sessions-{day}.jsonl").open("a") as f: +with (log_dir / f"sessions-{day}.jsonl").open("a", encoding="utf-8") as f: f.write(json.dumps(entry) + chr(10)) sys.exit(0) diff --git a/engine/hooks/session-logger.py b/engine/hooks/session-logger.py index a37e333..4099c92 100755 --- a/engine/hooks/session-logger.py +++ b/engine/hooks/session-logger.py @@ -23,6 +23,6 @@ "cwd": payload.get("cwd") or os.getcwd(), "transcript_path": payload.get("transcript_path"), } -with (log_dir / "sessions.log").open("a") as f: +with (log_dir / "sessions.log").open("a", encoding="utf-8") as f: f.write(json.dumps(entry) + chr(10)) sys.exit(0) diff --git a/engine/hooks/session-recap.py b/engine/hooks/session-recap.py index 1c9773c..0d8602d 100755 --- a/engine/hooks/session-recap.py +++ b/engine/hooks/session-recap.py @@ -68,7 +68,7 @@ def parse_transcript(path): if not path or not Path(path).exists(): return stats - with open(path, "r") as f: + with open(path, "r", encoding="utf-8") as f: for line in f: try: d = json.loads(line) @@ -206,9 +206,9 @@ def ensure_journal(day): f = JOURNAL_DIR / f"{day}.md" if not f.exists(): JOURNAL_DIR.mkdir(parents=True, exist_ok=True) - f.write_text(f"# {day} — Daily\n\n## 📊 Claude Sessions\n\n") - elif "## 📊 Claude Sessions" not in f.read_text(): - with f.open("a") as fh: + f.write_text(f"# {day} — Daily\n\n## 📊 Claude Sessions\n\n", encoding="utf-8") + elif "## 📊 Claude Sessions" not in f.read_text(encoding="utf-8"): + with f.open("a", encoding="utf-8") as fh: fh.write("\n## 📊 Claude Sessions\n\n") return f @@ -216,7 +216,7 @@ def ensure_journal(day): def upsert_session(journal, session_id, entry): """Replace existing block for this sid (or append). True if changed.""" sid_short = session_id[:8] - txt = journal.read_text() + txt = journal.read_text(encoding="utf-8") # Match existing block: header line + nested " - ..." continuation lines pattern = re.compile( rf"^- \*\*[^\n]+sid:`{re.escape(sid_short)}`[^\n]*\n(?: - [^\n]+\n?)*", @@ -228,7 +228,7 @@ def upsert_session(journal, session_id, entry): new_txt = new_txt.rstrip() + "\n\n## 📊 Claude Sessions\n\n" new_txt = new_txt.rstrip("\n") + "\n" + entry + "\n" if new_txt != txt: - journal.write_text(new_txt) + journal.write_text(new_txt, encoding="utf-8") return True return False @@ -276,7 +276,7 @@ def main(): # Log the error but never break the Stop hook err_log = Path.home() / ".claude" / "logs" / "session-recap-errors.log" try: - err_log.open("a").write(f"{time.strftime('%FT%T')} {type(e).__name__}: {e}\n") + err_log.open("a", encoding="utf-8").write(f"{time.strftime('%FT%T')} {type(e).__name__}: {e}\n") except Exception: pass sys.exit(0) From c9f2076ec1f20ae310d5275a4e06ba7401e09d20 Mon Sep 17 00:00:00 2001 From: Ismael Marechalle Date: Fri, 3 Jul 2026 17:19:40 +0200 Subject: [PATCH 3/3] install.ps1: register Task Scheduler jobs with S4U logon type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Register-ScheduledTask -User defaults to LogonType Interactive, which requires an active logged-in session. Verified on a real Windows machine: the 04:00 reindex and midday reflection both failed (0x800710E0, "the operator or administrator has refused the request") when triggered while the user wasn't actively logged in, and only succeeded when manually re-run during an interactive session. LogonType S4U runs the task whether the user is logged on or not, without needing to store a password — appropriate for these background/nightly jobs. --- install.ps1 | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/install.ps1 b/install.ps1 index 8a54502..1ce5a37 100644 --- a/install.ps1 +++ b/install.ps1 @@ -119,7 +119,12 @@ if ($cur -match [regex]::Escape($mark)) { Say "Task Scheduler (reindex 04:00 + reflection 12:00/23:00)" function Register-BrainTask($name, $argument, $triggers) { $action = New-ScheduledTaskAction -Execute $Py -Argument $argument - Register-ScheduledTask -TaskName $name -Action $action -Trigger $triggers -Force -User $env:USERNAME | Out-Null + # LogonType S4U : tourne meme si personne n'est connecte/deverrouille (pas de mot de passe stocke). + # Le defaut de Register-ScheduledTask -User est LogonType Interactive, qui exige une session active - + # observe en pratique : echec (0x800710E0, "operateur/administrateur a refuse la requete") au declenchement + # de nuit/verrouille, succes seulement quand relance manuellement en session active. + $principal = New-ScheduledTaskPrincipal -UserId $env:USERNAME -LogonType S4U -RunLevel Limited + Register-ScheduledTask -TaskName $name -Action $action -Trigger $triggers -Principal $principal -Force | Out-Null Ok "tache: $name" } try {