#4250·claude-mem

[Windows] agent-cost-report: render silently leaves a 0-byte report.html — plus 5 more findings and a 6-line patch

Author: meng823222Created Sep 27, 2026Updated Sep 27, 2026

First full end-to-end run of the rebuilt agent-cost-report (#4238) on Windows. The pipeline itself works (prices → collect → rollup → review → render → MCP evidence pass all complete), but stock Windows cannot produce report.html — and the two worst failures are silent: a 0-byte artifact with rc=0, and a review that applies nothing without erroring. A 6-line render.py patch got the whole chain green (diff at the bottom).

Environment: Windows 10 LTSC 2021 · Python 3.14.3 (no tzdata) · locale zh-CN → cp936 · claude-mem 13.27.0 (checked 13.28.0 release notes — no related fix listed) · invoked via Claude Code.

Findings (by severity)

1. Fatal — render.py: %-d is a glibc extension; Windows strftime raises

day_label() and date_pill() format no-pad day-of-month as %-d. On Windows: ValueError: Invalid format string (render.py:40 and the three f-strings at 47–49).

2. Fatal — render.py: report written without encoding= → 0-byte HTML, rc=0

After fixing #1: render_file() opens the report with open(path, "w") → cp936; the template's ‹ (U+2039) can't be encoded → UnicodeEncodeError — but a 0-byte report.html is left on disk and the step reports success. report.json / line-items.csv are written fine, so nothing looks wrong unless you check the HTML itself.

3. Serious — labels.py: template pre-fills reviewed_by: null; setdefault can't overwrite → applied 0 labels, no error

Filling the review into the template in place is a silent no-op (applied 0 labels; 0 of 1 reviewed, rc=0). Writing the same review into the item works (applied 1). The template's own placeholder defeats setdefault (it only fills missing keys).

4. Medium — missing encoding="utf-8" on reads: transcripts.py:54,96, behavior.py:125, wins.py:84, rules.py:35

open(f, errors="ignore") without encoding → cp936 decodes UTF-8 as mojibake. Dollar figures are unaffected (numeric fields), but the behavior pass degrades: human turns tagged author:"bot", wrong frustration/hedging detection, garbled excerpts.

5. Medium — render.py:498,506: Chrome detection can never match on Windows

chrome="google-chrome" + shutil.which() — on Windows the binary is chrome.exe and Chrome is not on PATH, so PDF is always "gracefully skipped" even when Chrome is installed.

6. Medium (env) — period.py:13: ZoneInfo("America/Los_Angeles") raises at import on tz-less Windows Python

Stock Windows Python ships no IANA tz database → import-time crash before anything runs. Zero-install workaround used: PYTHONTZPATH=<Git for Windows>\mingw64\share\zoneinfo (or pip install tzdata). A hard dependency or a friendlier error would help Windows users.

Patch (fixes #1 and #2; render.py only, 6 changed lines)

diff
--- a/skills/agent-cost-report/scripts/acr/render.py
+++ b/skills/agent-cost-report/scripts/acr/render.py
@@ -37,16 +37,16 @@
 
 def day_label(day, short=False):
     d = dt.date.fromisoformat(day)
-    return d.strftime("%b %-d") if short else d.strftime("%a %b %-d")
+    return f"{d:%b} {d.day}" if short else f"{d:%a %b} {d.day}"
 
 
 def date_pill(window, scope):
     """"Sep 18 – 25, 2026 (PT)"; cross-month "Sep 29 – Oct 2, 2026 (PT)"; one day "Sep 12, 2026 (PT)" (mapping #1)."""
     if scope.get("kind") == "session" and not window.get("start_pt"): return "one session"
     a = dt.date.fromisoformat(window["start_pt"]); b = dt.date.fromisoformat(window["end_exclusive_pt"]) - dt.timedelta(days=1)
-    if a == b: return a.strftime("%b %-d, %Y (PT)")
-    if a.month == b.month: return f"{a.strftime('%b %-d')} – {b.day}, {b.year} (PT)"
-    return f"{a.strftime('%b %-d')} – {b.strftime('%b %-d')}, {b.year} (PT)"
+    if a == b: return f"{a:%b} {a.day}, {a.year} (PT)"
+    if a.month == b.month: return f"{a:%b} {a.day} – {b.day}, {b.year} (PT)"
+    return f"{a:%b} {a.day} – {b:%b} {b.day}, {b.year} (PT)"
 
 
 def cat_sums(items):
@@ -487,11 +487,11 @@
 # ---- entry points (plan 3.5) ----
 def render_file(inp, outdir, print_mode=False):
     import json, os
-    with open(inp) as fh: d = json.load(fh)
+    with open(inp, encoding="utf-8") as fh: d = json.load(fh)
     os.makedirs(outdir, exist_ok=True)
     name = "report.print.html" if print_mode else "report.html"
     path = os.path.join(outdir, name)
-    with open(path, "w") as fh: fh.write(page(d, print_mode))
+    with open(path, "w", encoding="utf-8") as fh: fh.write(page(d, print_mode))
     return path

Evidence

  • Before patch: report.html 0 B, report.print.html 0 B (rc=0) · after patch: 22,921 B / 22,925 B.
  • review --apply: applied 0 labels (via template) → applied 1 (via item).
  • Rest of the chain on Windows is healthy: prices fetched 458 models · collect rows=15 / files_seen=196 / dedup_dropped=27 / 1,420,216 tokens · MCP evidence pass 6/6 · PDF skips gracefully (no Chrome in PATH).
  • Zero changes were made to the installed package; the patch above was validated on an isolated copy of the skill tree.