Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 40 additions & 1 deletion agentb/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import os
import sys
import io
import json
import signal
import subprocess
Expand All @@ -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"
Expand Down
16 changes: 14 additions & 2 deletions mnemo-wiki-compile.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand Down