diff --git a/agentb/cli.py b/agentb/cli.py index 22c6fd4..a3fa3ed 100644 --- a/agentb/cli.py +++ b/agentb/cli.py @@ -18,6 +18,7 @@ import os import sys +import io import json import signal import subprocess @@ -31,7 +32,45 @@ from rich.prompt import Prompt, Confirm from rich import print as rprint -console = Console() + +def _make_console() -> Console: + """Construct a Console that works whether stdout is an interactive + terminal or redirected to a file / pipe / Scheduled Task. + + Windows-specific bug worked around here: when stdout is redirected, + rich's writer still routes through ``_win32_console.LegacyWindowsTerm`` + which falls back to ``self.file.write()`` — i.e. Python's stdout + encoder. On Windows that defaults to ``cp1252``, which can't encode + the ⚡ emoji in the banner. The watcher dies before it ever runs, + silently breaking auto-capture under Task Scheduler / piped SSH / + any non-interactive context. + + For interactive TTYs (anywhere) we leave rich on its defaults so + colors / boxes / progress bars work as designed. For non-TTY stdout + we wrap the underlying buffer in a UTF-8 ``TextIOWrapper`` with + ``errors='replace'`` (so even genuinely un-encodable chars degrade + rather than crash) and ask rich to skip terminal-mode writes + entirely via ``force_terminal=False``. + """ + is_tty = bool(getattr(sys.stdout, "isatty", lambda: False)()) + if is_tty: + return Console() + buffer = getattr(sys.stdout, "buffer", None) + if buffer is None: + # Exotic stdout replacement with no underlying byte buffer — + # safest move is plain rich on whatever this is. + return Console(force_terminal=False) + utf8_stdout = io.TextIOWrapper( + buffer, + encoding="utf-8", + errors="replace", + line_buffering=True, + write_through=True, + ) + return Console(file=utf8_stdout, force_terminal=False) + + +console = _make_console() CONFIG_DIR = Path.home() / ".config" / "agentb" CONFIG_FILE = CONFIG_DIR / "agentb.yaml" diff --git a/mnemo-wiki-compile.py b/mnemo-wiki-compile.py index dd63085..9fdc8e3 100755 --- a/mnemo-wiki-compile.py +++ b/mnemo-wiki-compile.py @@ -725,6 +725,10 @@ def main() -> int: try: state = load_state() + # Capture the cursor BEFORE harvest so memories that arrive during + # the compile loop aren't missed next run. + run_started_at = datetime.now(timezone.utc).isoformat() + # Time window if args.full: since = datetime(2000, 1, 1, tzinfo=timezone.utc) @@ -739,6 +743,13 @@ def main() -> int: memories = harvest_agentb(since) + harvest_mnemo_sqlite(since) log.info(f"Harvested {len(memories)} memories") if not memories: + # ADVANCE the cursor even on an empty harvest. Without this, + # a single zero-result night wedges the window forever: + # subsequent runs query the same stale `since`, find the same + # nothing, return early before save_state(), and the cursor + # never moves. Observed May 17 → May 24 wedge in production. + state["last_run"] = run_started_at + save_state(state) log.info("No new memories — nothing to compile") return 0 @@ -827,8 +838,9 @@ def main() -> int: for rel in audit_report["manually_edited"][:20]: log.info(f" manually-edited: {rel}") - # Persist run state - state["last_run"] = datetime.now(timezone.utc).isoformat() + # Persist run state — use the timestamp captured BEFORE harvest so + # memories arriving during the compile loop aren't missed next run. + state["last_run"] = run_started_at save_state(state) # Log summary