[Windows] agent-cost-report: render silently leaves a 0-byte report.html — plus 5 more findings and a 6-line patch
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)
--- 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 pathEvidence
- Before patch:
report.html0 B,report.print.html0 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:
pricesfetched 458 models ·collectrows=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.
Source: thedotmack/claude-mem